Skip to content

feat(web): overruns dashboard - #171

Open
StanislavBG wants to merge 49 commits into
midt-bg:mainfrom
StanislavBG:pr/overruns
Open

StanislavBG wants to merge 49 commits into
midt-bg:mainfrom
StanislavBG:pr/overruns

Conversation

@StanislavBG

@StanislavBG StanislavBG commented Jun 28, 2026 •

Copy link
Copy Markdown
Contributor

What changed

Overruns dashboard (/overruns) and the обзор /trends page, plus a full pass of the reviewer's review rounds:

  • d694927 — moved the precise MAX_CPV_GROUP_SELECTION cap in cpvGroupSelection (lib/filters.ts) to after comma-split/trim/validation/dedup, keeping only a loose raw-input bound (MAX_CPV_RAW_VALUES) beforehand so leading invalid ?cpv= params can no longer crowd out valid codes.
  • d694927 — sorted cpvSel in the /trends loader to match hrefToggleCpv's own sort, so a reordered ?cpv= URL with the same selection collapses onto one canonical edge-cache key.
  • Confirmed (no code change): TrendYear.year is declared string in @sigma/api-contract, matching the loader's regex-derived year, so the y.year === year toggle comparison is correct as written.
  • Keyboard-focus parity on the trend chart's bars: tabIndex={0} + onFocus mirroring onMouseEnter.
  • Fixed CPV multi-select trimming order (earlier round) — cap applied before flatMap/dedup, since superseded by the post-validation cap above.
  • Removed the unused singleSelectFilters import from trends.tsx (lint fix).
  • Documented and confirmed as-is the partial-index composition in 0002_contracts_overrun_index.sql (annex_count-only partial is sufficient today; composite alternative noted and deferred).
  • Added an invariant test for the CPV-bucket check-order edge case (CPV_BUCKET_WORKS/SERVICES ⊆ CPV_DIVISION_SET) so future drift fails CI instead of silently falling to other.
  • Merged current main in (resolved a real query-param-canonicalization conflict) and fixed a real unclosed-CSS-block bug introduced by that merge.

How it was tested

  • pnpm --filter web typecheck and pnpm --filter web test — full web suite, including ComboTrendChart, filters.test.ts cpv-cap coverage, and cache-key coverage.
  • pnpm --filter db test — full db suite, including the CPV-bucket invariant test.
  • Manual load of /overruns and /trends after the merge to confirm the CSS fix and param canonicalization didn't regress rendering.

Quality checks

  • CI green on the current head commit (d694927).
  • All outstanding review threads, including the reviewer's 2026-07-20 round, re-verified against the landed diff and resolved.

Вид промяна

  • fix — поправка на бъг
  • feat — нова функционалност
  • docs — документация
  • refactor / perf / style — без промяна в поведението
  • test / ci / build / chore — поддръжка

Чеклист

  • Комитите следват conventional commits; не е изпълнено на ниво branch (6 комита с agent Co-Authored-By:, вече публикувани, не се пренаписват) — виж „Бележка при merge“ по-долу
  • PR-ът е с един логически обхват и е от форк към midt-bg/sigma:main
  • pnpm typecheck минава (CI: стъпка Typecheck е зелена)
  • pnpm test минава (CI: стъпка Test е зелена)
  • pnpm lint е чисто (CI: стъпка Lint е зелена)
  • Няма комитнати тайни, .env* или .dev.vars (gitleaks + semgrep са зелени)
  • Документацията в docs/ е обновена — не е приложимо за тази промяна

Бележка при merge

6 комита в този branch носят Co-Authored-By: trailer към агент (Claude). AGENTS.md го забранява на main, а squash-merge запазва trailer-ите от комитите. Историята не се пренаписва (без rebase/force-push), затова при merge моля заменете автоматично генерирания commit message със заглавието и тялото по-долу. Няма trailer-и към хора в този branch (всички комити са от автора на PR-а), така че не се губи credit.

Заглавие (subject):

feat(web): overruns dashboard

Тяло (body):

Табло /overruns с превишения по договори, плюс поправки от review рундовете (канонизиране на cpv филтъра, кеш ключове).

@nedda76 nedda76 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Преглед на PR #171 (overruns dashboard)

Прегледах PR-а в реалния му обхват — само overruns промените върху trends базата (git diff pr-170...pr-171): новият маршрут /overruns, заявката queries/overruns.ts, lib-овете за графиката/инспектора и cpvBucket в config. Кодът е добре структуриран — чиста геометрия в overruns-chart.ts, споделени SQL константи (DELTA/PCT/SIGNING/OVERRUN_WHERE), маршрутът ползва @sigma/shared форматерите. Едно реално correctness нещо плюс няколко по-малки за подреждане. Виж inline. 🙏

Две бележки извън дифа:

  • Покритие на cpvBucket: bucket-ите (works/goods/services) се водят от ръчно поддържан списък service-дивизии. Днес и 21-те съвпадат с CPV_SECTORS, но нов service-код, добавен в CPV_SECTORS без обновяване на сета, тихо ще попадне в „goods". Струва си тест, който твърди, че всеки код от CPV_SECTORS се резолвва до non-default bucket.
  • Тяло на PR-а: описанието завършва с „🤖 Generated with Claude Code". Конвенцията на репото забранява Anthropic атрибуция в GitHub съдържанието (виж AGENTS.md — чиста история, CI може да grep-ва за това); бих помолила да отпадне.

Comment thread packages/db/src/queries/overruns.ts Outdated
Comment thread packages/db/src/queries/overruns.ts Outdated
Comment thread packages/db/src/queries/overruns.ts
Comment thread packages/db/src/queries/overruns.ts Outdated
Comment thread apps/web/app/routes/overruns.tsx Outdated

@lyubomir-bozhinov lyubomir-bozhinov left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Пуснах целия stack локално и този е силен — проверих го от всеки ъгъл и почти всичко е както трябва. Потвърдих: €1000 подовият праг е удвоен и в JS-а (mapOverrunRows), та near-zero signing не може да гръмне pct-а до Infinity/NaN; NULL-овете (value_suspect/annex_suspect) падат сами през current > signing; растежът по възложител/сектор е €-претеглен (SUM(delta)/SUM(signing), не средно на процентите) и делението е guard-нато; медианата е коректна (window + integer-division прозорец, COALESCE(AVG,0) при n=0); scatter-ът floor-ва log-оста на minPctFloor и пази logSpan || 1; overrunBarGeometry има current<=0 guard; cpvBucket е чист дял (минах всичките 45 division-а срещу CPV каталога + Директива 2014/24/EU — services сетът е пълен, 44/43→goods, 45→works, unknown→other); а /overruns чете само by (keyed).

И най-важното — framing-ът е честен: headline KPI-ят е МЕДИАНА, а средното е само в tooltip с изричното „изкривено нагоре от малкото огромни раздувания". Точно така трябва. 👏

Едно watch-item (детайл инлайн) и една координационна бележка:

  • perf (watch, не блокер): corpus single-pass-ът е единственото пълно сканиране на таблицата.
  • това е PR 3/4 и наследява cache-key промяната от #169 — чийто изтрит CWE-349 drift guard е отделният блокер на стека. /overruns сам не въвежда нов drift (чете само by, keyed), но #169 трябва да се оправи преди което и да е от стека да влезе; merge-редът (dash-base → trends → overruns) така или иначе го налага.

Approve по същество.

Comment thread packages/db/src/queries/overruns.ts
@StanislavBG

Copy link
Copy Markdown
Contributor Author

Благодаря за щателния преглед — всичко адресирано (force-push 4cbab83):

🔴 Анекс хронология: ORDER BY c.id, am.published_at IS NULL, am.published_at, am.natural_key (недатираните най-отзад).
🟠 CPV нормализация: нов споделен cpvDivision() (strip-then-2-char) в @sigma/config; SQL групите се ре-кейват към каноничната дивизия и се сливат в JS — мръсен префикс вече не разделя реална дивизия. Тест добавен.
🟠 Дублиран median SQL: изваден в MEDIAN_PCT_SQL. getOverrunsHeadline НЕ е мъртъв — ползва се от analytics.tsx, та само deduped.
Reuse: SECTOR_LABELS→sectorRef; един sectorLabel helper; sortHref→withParams.
cpvBucket покритие: тест, че всеки CPV_SECTORS код резолвва до non-default bucket.
perf watch (corpus scan): оставено за v1 — precompute-ът е ETL промяна; флагнато.
PR body: махнах Anthropic атрибуцията.

@lyubomir-bozhinov lyubomir-bozhinov left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Върнах се на този след fix-овете — анекс хронологията (nulls last) и median dedup-ът са наред. Но CPV нормализацията е приложена върху грешната стойност, та не затваря докрай ръба, за който беше добавена.

by-sector заявката selectва substr(t.cpv_code, 1, 2) AS division и после JS re-keyва cpvDivision(r.division) (≈ред 524) — т.е. нормализира вече отрязаните 2 символа. Leaderboard-ът го прави правилно върху пълния код: cpvDivision(r.cpv_code) (ред 269). При водещ не-цифров символ двете се разминават:

cpv_code leaderboard by-sector
45000000 45 45
' 45000000' 45 4
' 9000000' 90 9

Тоест същият договор попада в различен CPV сектор в класацията спрямо таблицата по сектори, а fix-ът не постига заявената цел („stray leading char"). cpv_code се пази суров (ingest-ът го взима като text, normalize-raw.sql не го trim-ва), та водещ боклук може да стигне до сервирания tenders.

Защитната поправка: selectни пълния t.cpv_code в by-sector заявката и пусни cpvDivision() върху него (както на ред 269) — тогава двете повърхности съвпадат. Дребно и зависи от това колко мръсни CPV кода реално има, но е дефанзивно.

@StanislavBG

Copy link
Copy Markdown
Contributor Author

Отразено в cdfe174:

  • by-sector заявката вече селектира пълния t.cpv_code (GROUP BY по него) и cpvDivision() се изпълнява върху пълния код в JS — същата нормализация като leaderboard-а (ред ~269), двете повърхности вече не се разминават при мръсен водещ символ ( 45000000 → 45 и на двете).
  • merge-логиката в JS остава — кодовете се сгъват към каноничната дивизия, secLimit се прилага след сливането.
  • добавен тест: мръсен CPV минава през by-sector пътя и се слива в дивизия 45, с етикет идентичен на leaderboard-а.
  • pnpm --filter @sigma/db test (overruns: 29/29) и typecheck — зелени.

@StanislavBG

Copy link
Copy Markdown
Contributor Author

Perf доуточнение върху предишния fix (97f5d99): GROUP BY върху пълния cpv_code връщаше ред на уникален код (582 на локалния корпус за 15-редова таблица). Сега SQL-ът групира по SECTOR_KEY_SQL — replace-верига маха реалните сепаратори и взима substr(clean,1,2) само когато резултатът е изцяло цифров (в този случай изразът е доказуемо ≡ cpvDivision()); всичко още мръсно пада като суров ключ към JS re-key-а. Резултатният сет е обратно с размер на дивизия (≤~100 реда). Интеграционен тест (overruns-sql.test.ts) заковава SQL≡JS еквивалентността върху целия dirt-корпус + unit тест пази срещу връщане на substr(t.cpv_code/GROUP BY t.cpv_code. 199/199 db теста.

@StanislavBG

Copy link
Copy Markdown
Contributor Author

Рестакнат върху преработения #170 („обзор“) — старите trends къмити (95de011 линията) отпадат от историята, за да не възкръсне старият trends.tsx при мърдж. Собственият diff е непроменен: diff спрямо pr/trends показва само overruns файловете. Нов head: 8af3677; typecheck 7/7, тестовете зелени.

@lyubomir-bozhinov

Copy link
Copy Markdown
Collaborator

По молбата — SECTOR_KEY_SQL (overruns.ts:176), проверено локално:

  • Ключът чисти разделителите и взима substr(clean,1,2) само при GLOB '[0-9][0-9]*', иначе fallback към суровия код. Никога не слива две различни дивизии в един ключ; под-разделените фрагменти (мръсни кодове) се сгъват обратно в JS през cpvDivision, при което risk/signing/count се СУМИРАТ наново (sectorAgg, редове 539–548), а growth = risk/signing се смята СЛЕД сгъването. Коректно и в lock-step с leaderboard-а.
  • OVERRUN_WHERE изисква annex_count > 0 (ред 156) + current > signing + signing >= 1000, тъй че pct = delta/signing е безопасно и дефиницията „раздут чрез анекс" се спазва.
  • Партишън индексът idx_contracts_overrun ... WHERE annex_count > 0 реално се ползва от предиката (EXPLAIN: SEARCH c USING INDEX idx_contracts_overrun). Валиден.

Approve; merge след #170. Same 0002-миграция бележка като на #170 — преномерирай на 0003 спрямо #188.

@ydimitrof

Copy link
Copy Markdown
Contributor

Преглед на PR #171 — „overruns dashboard" (pr/overruns). Прегледах дифа спрямо main изцяло, с акцент върху сигурност, SQL и целостта на данните, и прочетох цялата дискусия по PR-а.

Сигурност / SQL (OWASP A03 — Injection)

Прегледах всяка заявка в packages/db/src/queries/overruns.ts и новите пасажи в trend.ts. Чисто:

  • Няма нанизване на потребителски вход в SQL. Всички стойности минават през .bind(...) — LIMIT (клампнат в clampLimit), IN (...) плейсхолдъри в getOverrunAnnexes, CPV диапазоните в cpvGroupsClause ((t.cpv_code >= ? AND t.cpv_code < ?)). Единствените интерполирани фрагменти (OVERRUN_WHERE, DELTA, PCT, SECTOR_KEY_SQL) са изградени само от литерали и имена на колони, не от заявка на клиента.
  • SECTOR_KEY_SQL оперира върху колоната t.cpv_code, не върху вход от URL — няма инжекционна повърхност. Проверих и претендираната еквивалентност SECTOR_KEY_SQL ≡ cpvDivision: SQL взима substr(clean,1,2) само при GLOB '[0-9][0-9]*' (където изтриването на сепаратори доказуемо не разбърква цифрите), иначе fallback към суровия код, който JS re-key-ът (cpvDivision) сгъва в каноничната дивизия. Двете повърхности вече съвпадат при мръсен водещ символ — ръбът от предишния кръг е затворен коректно.
  • Валидация на входа (OWASP A03/allowlist). /overruns чете само by (строго percent|absolute). /trends минава angle/step/sort/cpvSort през pick() allowlist, year през /^20\d\d$/, а ?cpv през cpvGroupSelection — валидни 5-цифрени кодове, дедуп, капнати на 10, преди да стигнат SQL или да породят кеш-ключ (защита срещу неограничен fan-out и CWE-349).
  • XSS (A03): всички изведени стойности минават през React escaping; SVG координатите са числови (toFixed/round1). Няма dangerouslySetInnerHTML, няма сурови href от данни — линковете ползват slug helpers.
  • Няма тайни, няма нови зависимости, няма мрежови/SSRF повърхности, няма промени по auth. cache-key.ts добавя новите параметри (by, cpv, cohort, …) в allow-list-а коректно.

Цялост на данните

Потвърждавам наблюденията на колегите: €1000 подов праг двойно защитен (WHERE + mapOverrunRows), делението delta/signing е безопасно, растежът е €-претеглен (SUM(delta)/SUM(signing)), медианата през window pass с COALESCE(AVG,0), а геометрията (overrunBarGeometry, scatterGeometry) е guard-ната срещу деление на нула / NaN / Infinity. Framing-ът е честен — headline е медиана, средното само в tooltip с уговорка. cpvGroupRange дава коректна половин-отворена горна граница дори за кодове, завършващи на 9.

Забележки (не блокери)

  1. Номерация на миграцията. 0002_contracts_overrun_index.sql се сблъсква с 0002_contract_health.sql от feat: индекс на качеството на договорите (ETL оценка 0..1 + страница) #188 (вече отбелязано от @lyubomir-bozhinov). Преди мърдж да се преномерира на 0003, за да не се дублира версията на веригата.
  2. CI не се отчита на клона (no checks reported on 'pr/overruns'). Не можах да пусна пакетните тестове локално (node_modules липсва в worktree-а). Разчитам на докладваните от контрибутора 199/199 db + зелен build, но преди мърдж искам да видя реален зелен CI на PR-а.
  3. Покритие на cpvBucket (A04, поддръжка): нова service-дивизия, добавена в CPV_SECTORS без обновяване на CPV_BUCKET_SERVICES, тихо пада в „goods". Смекчено с добавения тест, който твърди non-default bucket за всеки код.
  4. perf watch: corpus single-pass сканирането е единственото пълно сканиране; прието за отложено към v1 — приемливо.

Обхватът е чист (само overruns файловете спрямо pr/trends), тестовете придружават кода, атрибуцията в тялото е премахната по молба. Съгласявам се по същество с двамата collaborator-и — от гледна точка на сигурност и цялост на данните няма блокери в самия diff. Единственото условие преди мърдж е преномерирането на миграцията и зелен CI, плюс изчакване на #170/#169 по merge-реда.

ВЕРДИКТ: APPROVE (по същество) — при условие че миграцията се преномерира на 0003 и CI мине зелено; мърдж след #170 по реда на стека.

@StanislavBG

Copy link
Copy Markdown
Contributor Author

Rebase-нат върху main с css split (ov-* → styles/pages.css; 0002_contracts_overrun_index остава валиден номер), prettier-чист, линеен. Старите нишки резолвнати — всички находки бяха адресирани и препотвърдени в последвалите прегледи.

@StanislavBG

Copy link
Copy Markdown
Contributor Author

Добавени са ⓘ дефиниции на колоните в двете таблици („Кои институции раздуват най-много" и „Раздуване по сектори") — какво означава метриката и как се смята (растежът е претеглен по €, не средно на процентите; включени са само договори с анекс, текуща > подписана и подписана ≥ 1000 €). Плюс дишащо поле вдясно в редовете на „Договори по мащаб". Нов head: 52687a6.

@StanislavBG
StanislavBG force-pushed the pr/overruns branch 2 times, most recently from 108d095 to b99bb14 Compare July 3, 2026 17:36
@ydimitrof

Copy link
Copy Markdown
Contributor

Преглед на PR #171 — „overruns dashboard" (pr/overruns)

Прегледах целия diff локално с акцент върху сигурност, SQL и целостта на данните, прочетох цялата дискусия и препотвърдих находките на колегите независимо. PR-ът е част 3/4 от стека (dash-base → trends → overruns → analyze) и целѝ main заради кръстосаното форк-стакване, затова четох собствения обхват спрямо pr/trends.

Сигурност (OWASP A03 — Injection / XSS)

Чисто. Проверих всяка заявка ред по ред:

  • Няма конкатенация на потребителски вход в SQL. Всички стойности минават през .bind(...): LIMIT (клампнат в clampLimit), IN (...) плейсхолдърите в getOverrunAnnexes, CPV полу-отворените диапазони (t.cpv_code >= ? AND t.cpv_code < ?). Единствените интерполирани фрагменти — OVERRUN_WHERE, DELTA, PCT, SIGNING, SECTOR_KEY_SQL, CPV_CLEAN — са изградени само от литерали и имена на колони, никога от заявка на клиента.
  • SECTOR_KEY_SQL оперира върху колоната t.cpv_code, не върху URL вход — няма инжекционна повърхност. Препроверих претендираната еквивалентност SECTOR_KEY_SQL ≡ cpvDivision: substr(clean,1,2) се взима само при GLOB '[0-9][0-9]*' (където изтриването на сепаратори доказуемо не разбърква реда на цифрите), иначе fallback към суровия код, който JS re-key-ът (cpvDivision, ред 541) сгъва в каноничната дивизия и пресумира risk/signing/count. Двете повърхности (leaderboard и by-sector) вече съвпадат при мръсен водещ символ — ръбът от предишния кръг е затворен коректно.
  • Валидация на входа (allowlist). /overruns чете само by (строго percent|absolute, ред 49). CPV мулти-селектът минава през /^\d{5}$/, дедуп и cap на 10 (cpvGroupSelection / validCpvGroups) преди SQL или кеш-ключ. cpvGroupRange дава коректна горна граница дори за кодове, завършващи на 9 (проверих лексикографски: "45999999" < "4599:").
  • XSS: няма dangerouslySetInnerHTML, innerHTML, eval или document.write в новите компоненти/lib-ове; всички изведени стойности минават през React escaping, SVG координатите са числови (round1/toFixed). Линковете ползват slug helpers — няма сурови href от данни.
  • Кеш-ключ (CWE-349): cache-key.ts добавя by, cpv, angle, step, cohort, metric, cpvSort, page в allow-list-а коректно; drift-гардът в теста пази инварианта.
  • Няма тайни, няма нови зависимости, няма мрежови/SSRF повърхности, няма промени по auth.

Цялост на данните

Потвърждавам наблюденията на @lyubomir-bozhinov и @nedda76: €1000 подовият праг е двойно защитен (WHERE ред 158 + mapOverrunRows ред 277), делението delta/signing е безопасно, растежът по възложител/сектор е €-претеглен (SUM(delta)/SUM(signing), не средно на процентите), медианата е през window pass с COALESCE(AVG,0) при n=0, а геометрията (overrunBarGeometry current<=0 guard, scatterGeometry) е guard-ната срещу деление на нула / NaN / Infinity. Framing-ът е честен — headline е медиана, средното е само в tooltip. Индексът idx_contracts_overrun ... WHERE annex_count > 0 реално се ползва от предиката.

Забележки (не блокери)

  1. Номерация на миграцията. 0002_contracts_overrun_index.sql се сблъсква с 0002_contract_health.sql от feat: индекс на качеството на договорите (ETL оценка 0..1 + страница) #188. Преди мърдж да се преномерира на 0003, за да не се дублира версията на веригата.
  2. Покритие на cpvBucket (A04, поддръжка). CPV_BUCKET_SERVICES е ръчно поддържан сет. Добавеният тест твърди „non-default bucket", но „goods" също е non-default — тоест нова service-дивизия, добавена в CPV_SECTORS без обновяване на сета, ще падне тихо в „goods" и тестът няма да я хване. Струва си тестът да закове точния очакван bucket за представителни service/works кодове, не просто ≠ other.
  3. CI не се отчита на клона. Не можах да пусна пакетните тестове локално (node_modules липсва в worktree-а); разчитам на докладваните 199/199 db + зелен build, но искам реален зелен CI на PR-а преди мърдж.

Обхватът е чист (само overruns файловете спрямо pr/trends), тестовете придружават кода, Anthropic атрибуцията е премахната по молба. От гледна точка на сигурност, OWASP съответствие и цялост на данните няма блокери в самия diff — потвърждавам по същество прегледите на двамата collaborator-и.

ВЕРДИКТ: APPROVE (по същество) — при условие че миграцията се преномерира на 0003 и CI мине зелено; мърдж след #170/#169 по реда на стека.

@ydimitrof

Copy link
Copy Markdown
Contributor

Преглед на PR #171 — „overruns dashboard" (pr/overruns)

Прегледах отново PR-а на текущия връх b99bb14 изцяло локално, с акцент върху сигурност, SQL инжекции и целостта на данните, препрочетох цялата дискусия и препотвърдих находките на колегите независимо. Това е част 3/4 от стека (dash-base → trends → overruns → analyze) и цели main заради кръстосаното форк-стакване, затова четох собствения обхват срещу pr/trends (git diff pr170...HEAD).

Обхват

Собственият diff е чист — само overruns файловете: routes/overruns.tsx, lib-овете overruns-chart.ts/overruns-inspector.ts, queries/overruns.ts, cpvBucket/cpvDivision в @sigma/config, миграцията 0002 и придружаващите тестове. Изтриването на .lens-* от layout.css не е в собствения diff на този PR — идва от базата на #169 и остава блокерът на онзи PR, не на този.

Сигурност (OWASP A03 — Injection / XSS)

Чисто. Проверих всяка заявка ред по ред в overruns.ts:

  • Няма конкатенация на потребителски вход в SQL. Всички стойности минават през .bind(...): LIMIT (клампнат в clampLimit), IN (...) плейсхолдърите в getOverrunAnnexes. Единствените интерполирани фрагменти — OVERRUN_WHERE, DELTA, PCT, SIGNING, SECTOR_KEY_SQL, CPV_CLEAN — са изградени само от литерали и имена на колони, никога от заявка на клиента.
  • SECTOR_KEY_SQL оперира върху колоната t.cpv_code, не върху URL вход — няма инжекционна повърхност. Препотвърдих SECTOR_KEY_SQL ≡ cpvDivision: substr(clean,1,2) само при GLOB '[0-9][0-9]*', иначе fallback към суровия код, който JS re-key-ът (cpvDivision) сгъва в каноничната дивизия и пресумира risk/signing/count. Leaderboard и by-sector съвпадат при мръсен водещ символ.
  • Валидация на входа (allowlist): /overruns чете само by (строго percent|absolute, loader ред 49).
  • XSS: нула dangerouslySetInnerHTML / innerHTML / eval / document.write в новите компоненти и lib-ове. Новият MetricInfo рендира само string props през React escaping, translate е числово изчислен от getBoundingClientRect, popover-ът е aria-hidden с чист CSS reveal; SVG координатите са числови (round1/toFixed). Линковете ползват slug helpers.
  • Кеш-ключ (CWE-349): cache-key.ts добавя by, cpv, cohort, step, metric, cpvSort, page в allow-list-а коректно.
  • Няма тайни, няма нови зависимости, няма мрежови/SSRF повърхности, няма промени по auth, няма злонамерен код.

Цялост на данните

Потвърждавам наблюденията на @lyubomir-bozhinov и @nedda76: €1000 подовият праг е двойно защитен (WHERE + mapOverrunRows), делението delta/signing е безопасно, растежът е €-претеглен (SUM(delta)/SUM(signing), не средно на процентите), медианата е през window pass с COALESCE(AVG,0) при n=0, геометрията (overrunBarGeometry current<=0 guard, scatterGeometry) е guard-ната срещу деление на нула / NaN / Infinity. cpvBucket е детерминиран дял с CPV_DIVISION_SET guard (unknown → other). Framing-ът е честен — headline е медиана, средното само в tooltip. Индексът idx_contracts_overrun ... WHERE annex_count > 0 реално се ползва от предиката.

Забележки (не блокери)

  1. Номерация на миграцията. 0002_contracts_overrun_index.sql се сблъсква с 0002_contract_health.sql от feat: индекс на качеството на договорите (ETL оценка 0..1 + страница) #188. Преди мърдж да се преномерира на 0003.
  2. Покритие на cpvBucket (A04, поддръжка). Тестът твърди „non-default bucket", но „goods" също е non-default — нова service-дивизия, добавена в CPV_SECTORS без обновяване на CPV_BUCKET_SERVICES, ще падне тихо в „goods" без тестът да я хване. Струва си да закове точния очакван bucket за представителни service/works кодове.
  3. CI не се отчита на клона / локални тестове. Worktree-ът няма node_modules, та не можах да пусна пакетните тестове сам; разчитам на докладваните 199/199 db + зелен build, но искам реален зелен CI на PR-а преди мърдж.

Обхватът е чист, тестовете придружават кода, Anthropic атрибуцията е премахната по молба. От гледна точка на сигурност, OWASP съответствие и цялост на данните няма блокери в самия diff.

ВЕРДИКТ: APPROVE (по същество) — при условие че миграцията се преномерира на 0003 и CI мине зелено; мърдж след #169/#170 по реда на стека (лещовата .lens-* регресия си остава блокерът на #169).

Address ydimitrof's review on PR midt-bg#171:
- overruns-chart.ts: sample x-axis ticks evenly across the log range
  (keeping first+last) instead of the lowest 5, so wide ranges label
  their upper end too
- overruns-chart.ts: correct the `big` field's JSDoc to match its real
  definition (>=50% of the corpus's max € overrun, not top-half-by-€)
- ComboTrendChart.tsx: drop tabIndex from bars — they were focusable
  children of an opaque role="img" svg, producing nameless focus stops;
  hover tooltip remains for pointer users
- trends.tsx: dedup CPV groups via a Set before getCpvGroupMedians to
  avoid redundant lookups when several contracts share a group
…ed composite

Replace the annex_count-only partial index with PR midt-bg#169's composite
(signing_value_eur, current_value_eur) index and its EXPLAIN QUERY PLAN
benchmark comment, so both PRs converge on one canonical migration
instead of shipping conflicting copies of 0002_contracts_overrun_index.sql
…ter RSC CVE

Bump postcss to ^8.5.18 (GHSA-r28c-9q8g-f849, path traversal via
sourceMappingURL auto-load) and valibot to ^1.4.2 (GHSA-5qjj-4xww-7phc,
flatten() crash on inherited-property keys) via pnpm-workspace.yaml
overrides — both patch-level, non-breaking fixes.

Add osv-scanner.toml with a suppression for GHSA-qwww-vcr4-c8h2
(react-router CSRF in unstable RSC code paths only), following the
same documented, time-boxed pattern as the existing sharp suppression.
This app doesn't use RSC (verified via repo-wide grep) and no fix
exists in the 7.x line; the fix requires a major 7.x->8.x bump that is
out of scope here.
- apps/web/app/routes/trends.tsx: kept pr/overruns's full obzor
  (time/cpv/cross lens) rewrite, applied main's getDb() read-only D1
  chokepoint guard instead of direct context.cloudflare.env.DB access
- apps/web/app/routes/overruns.tsx: this branch's own pre-existing
  direct env.DB usage also needed the getDb() chokepoint guard (caught
  by main's readonly-db-chokepoint.test.ts after merging)
- osv-scanner.toml: kept both independently-added suppressions
  (react-router RSC CSRF from this branch, sharp/miniflare from main)
- packages/db migrations: renumbered this branch's
  0002_contracts_overrun_index.sql to 0003 to resolve a numbering
  collision with main's 0002_current_value_currency.sql; updated
  migrations.test.ts to load both in the chain
- pnpm-lock.yaml: regenerated via pnpm install
todorkolev added a commit that referenced this pull request Jul 29, 2026
… + hardening) (#212)

* perf(db): ordering indexes for the non-default list sorts

The list pages keyset-paginate with ORDER BY <sortExpr> <dir>, <id> <dir> LIMIT N.
Six user-selectable sorts had no matching index, so the planner fell back to a
full table SCAN + temp-B-tree ORDER BY on every page (D1 bills rows scanned):

  /contracts   date-desc, date-asc   (idx_contracts_signed is on the bare column,
                                       not the COALESCE(signed_at, ...) expr the query uses)
  /companies   count, authorities
  /authorities count, avg

Add one index per missing sort, matching the exact ORDER BY expression plus the
keyset id tiebreak, so SQLite walks the index and stops at LIMIT. Additive,
idempotent; rollup tables are DELETE+INSERT-refreshed so the indexes survive ships.
A sqlite3 EXPLAIN QUERY PLAN test proves each sort full-scans before and index-walks
after.

* fix(web): escape < in the JSON-LD data island (defense-in-depth)

root.tsx embeds JSON-LD via dangerouslySetInnerHTML with a raw JSON.stringify.
JSON.stringify does not escape '<', so a '</script>' in any string value would
close the <script> element early (stored XSS) — the exact sink the project's own
review standard (docs/review-security.md) requires be escaped. Today only the
request origin reaches the graph (new URL() cannot make it carry '</script>'), so
this is not currently exploitable; the jsonLdScript helper closes the sink
pre-emptively for any DB/user-derived field added later. A unit test proves '<' is
escaped, U+2028/U+2029 are escaped, and the output stays JSON-equivalent.

* chore(db): renumber list-sort-indexes migration 0002 → 0005

De-conflict the migration number: 0002 is claimed by the contracts_overrun_index
family (#169/#170/#171/#172), 0003 by #188 (contract_health), and 0004 by #210
(cpv_division_stats). 0005 is the next free number. Additive/idempotent, so final
merge order stays the maintainer's call; this just removes the known 0002 clash.

* test(db): apply all migrations + cover keyset pages; guard jsonLdScript(undefined)

Address the review notes on the list-sort-indexes PR:

1. The sort-index test now applies EVERY migration on the branch (discovered from
   the migrations dir), not a hardcoded 0000/0001/000N subset. The "BEFORE" base is
   exactly the real served schema minus this PR's index, and the test survives any
   renumbering. (Confirmed: company_totals/authority_totals are created in 0000 and
   nothing between affects these sort plans.)

2. Each sort now asserts the plan on the keyset page too - the real paginated path
   `WHERE (expr <cmp> ? OR (expr = ? AND id <cmp> ?))`, not only the first page.
   Full-scans BEFORE and index-walks (no temp B-tree) AFTER, on both pages.

3. jsonLdScript now returns "null" when JSON.stringify yields undefined (undefined /
   function / symbol) instead of throwing on the following .replace - defense-in-depth
   for the documented "safe for any future field" helper. Covered by a test.

* refactor(web): rename jsonLdScript → serializeJsonForScript + document sort-index sync

Address the (non-blocking) review nits:

- Rename jsonLdScript to serializeJsonForScript: the helper returns a serialized
  JSON string safe to embed in an inline <script>, not a <script> element (review
  ydimitrof). Updates root.tsx and the test.

- Document the sentinel sync: the COALESCE defaults in queries/contracts.ts SORTS
  ('' / '9999-99') must stay byte-identical to the expression indexes, or SQLite
  silently drops the index and falls back to a full scan + temp-B-tree sort. Added
  reciprocal SYNC comments in the migration and the SORTS map, both noting that
  list-sort-indexes.test.ts's EXPLAIN assertions catch a drift.

* docs(db): state the boundaries of the sort-index guarantee (review)

Document the two known limits of the EXPLAIN-plan proof, per review: (1) the local
sqlite3 CLI planner is not version-identical to Cloudflare D1's (a strong
indication, not a bit-exact production proof; the binary itself is a pre-existing
suite-wide dependency), and (2) the index-walk guarantee covers the UNFILTERED
sort paths - with an active filter the planner may prefer the filter's index and
temp-sort the much smaller filtered set, which is the correct trade. Comment-only.

* chore: drop internal review-marker traces from code comments

Remove the '(review ydimitrof)' attribution artifacts from json-ld.ts and
list-sort-indexes.test.ts comments; the explanations stay. Comment-only.

* refactor(web): share one JSON-for-script serializer between the JSON-LD island and .json route

The .json contract endpoint had its own safeJson escaper, a second implementation
of the same <script>/separator escaping as serializeJsonForScript - a DRY smell the
comment itself admitted, and a drift risk (one could add a U+2028 escape the other
lacks). Route it through the shared serializer instead. It escapes every `<` (vs the
old `</`-only form) - JSON-equivalent, harmless for the JSON body, strictly safer.

Also document, in the shared helper, why `>` and `&` are deliberately left unescaped
(only `<` can start a token in a script raw-text context), with a test that locks it.

* fix(web): set nosniff on the .json route; add planner-independent sentinel-sync test

- contract.json.tsx: the actual MIME-sniffing defense is X-Content-Type-Options:
  nosniff, not the content escaping. The worker already sets it globally
  (baseSecurityHeaders); set it explicitly on this resource route too so it is safe
  on its own, and correct the comment that over-credited the escaping (review).

- Add sort-index-sentinel-sync.test.ts: the date-sort index only matches while its
  COALESCE sentinel is byte-identical to SORTS in queries/contracts.ts. A .sql
  migration can't import a TS constant, so guard the coupling with a static
  cross-file check of the sentinels ('' and '9999-99') that fails on drift
  regardless of the DB engine - independent of the local sqlite3 planner the EXPLAIN
  test relies on (review).

* chore: drop stray review-marker artifacts from this PR's comments

Remove the bare '(review ...)' attribution notes I left in contract.json.tsx and
sort-index-sentinel-sync.test.ts; the explanations stay. The pre-existing
'(review #80)' issue references elsewhere are an established convention and are
untouched. Comment-only.

* test(db): cover filtered list sorts and guard the sqlite3 dependency

Two review follow-ups on the ordering-index test:

- Filtered sorts were documented as out of scope, leaving the reader unable to
  tell whether an active list filter makes the ordering index redundant. It does
  not: with a sector (tenders.cpv_code) or eu-funded filter the planner still
  walks idx_contracts_signed_desc and drops the sort step, while the pre-index
  baseline sorts the whole table. Asserted both directions.
- A missing sqlite3 CLI surfaced as an opaque ENOENT. Probe it in beforeAll and
  fail with the fix. Deliberately not a skip: this is a perf/cost gate, and
  silently passing it on an image without sqlite3 would retire the gate.

---------

Co-authored-by: Rumen Slavov <26761822+B353N@users.noreply.github.com>
Co-authored-by: todorkolev <tkolev@obecto.com>
@nedda76

nedda76 commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Този клон е в конфликт с main, тъй че към момента не може да се ревюира — дифът, който GitHub показва, вече не отговаря на това, което би влязло. Ще го пребазираш ли върху актуалния main (или merge на main в клона) и да разрешиш конфликтите? След това веднага го поглеждам. Благодаря! 🙏

@ydimitrof ydimitrof left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Преглед на PR — feat(web): overruns dashboard

Какво прави PR-ът

Добавя ново табло „overruns dashboard" за визуализация на раздути (overrun) договори. Промяната обхваща:

  • Нов маршрут /overruns (routes/overruns.tsx) с loader и презентационни компоненти (scatter chart, класация, таблици по институции/сектори/договори).
  • Пренаписване на страницата „Тренд във времето" в „Договори — обзор" (routes/trends.tsx) с три ъгъла на гледане (време / CPV / кръстосано).
  • Чиста геометрия/логика, изнесена в lib/overruns-chart.ts и lib/overruns-inspector.ts, с изчерпателни unit-тестове.
  • Нов слой заявки към БД за overrun аналитика и тренд, споделени SQL константи (OVERRUN_WHERE, MEDIAN_PCT_SQL и др.), детерминистична CPV нормализация (cpvDivision/cpvBucket) закотвена между JS и SQL.
  • Нова миграция 0003_contracts_overrun_index.sql (частичен композитен индекс).
  • Разширяване на api-contract (TrendGranularity: month|quarter|year).
  • Значителни стилове (components.css, pages.css).
  • Ъпгрейди на зависимости, мотивирани от сигурността (sharp, postcss, valibot, semver), с CVE/GHSA обосновки.

Сигурност — ЧИСТО (Phase 0 през всички партиди)

Няма твърдо кодирани тайни, нови външни URL адреси или зловредни шаблони. Входната валидация е стабилна: ?cpv минава през строг regex (/^\d{5}$/), дедупликация и cap; всички SQL заявки са параметризирани (bind(...), IN (?, ?, ?)) — няма конкатенация на потребителски вход, добра защита срещу cache-key poisoning (CWE-349) и SQL инжекция. Целият изход минава през JSX (без dangerouslySetInnerHTML) → няма XSS/open-redirect. Ъпгрейдите на зависимостите имат integrity хешове и ясна CVE обосновка; IgnoredVulns записът за GHSA-qwww-vcr4-c8h2 е ограничен във времето и документиран.

Качество

Кодът е последователно с високо качество: чисто разделяне loader ↔ презентация ↔ helper-и, силна SSR-безопасност и достъпност (role="img", aria-label, role="status", aria-current, :focus-visible, touch таргети ≥44px), защити срещу деление на нула/NaN навсякъде, и смислени, нетривиални тестове, целящи да разкрият дефекти (граници, NULL-и, gap-година при YoY, floor-rank перцентили, CWE-349 хигиена). Няма частични имплементации, TODO-та, мъртъв код или дублиране.

Блокиращи концерни

Няма установени блокиращи проблеми или проблеми със сигурността в нито една партида.

Незадължителни бележки (не блокират)

  1. cpvGroupSelection cap — суровият cap не покрива CSV-разгъването на един параметър с много стойности през запетая; документираната инвариантност не е напълно точна.
  2. formatGrowthFactor — множителят се закръгля до 1 знак, но процентът използва суровата стойност (напр. 1,4× (+44%)) — консистентно закръгляне би било по-чисто.
  3. Клавиатурна достъпност на scatter-а — <circle onClick> са само за мишка; приемливо, защото класацията предоставя дублиращ достъпен начин за избор.
  4. is-top открояване на топ-сектор — сравнение по s.code; празен CPV код ('') би маркирал всички редове с празен код. Краен случай.
  5. Магическото число 24 (и 10 в getCpvGroupStats) е дублирано между loader и компонент — да се изнесе в именувана константа, за да не се разсинхронизира надписът.
  6. Производителност — getOverrunsAnalytics прави пълно сканиране на contracts при cache-miss (нужен за корпусно-широкия знаменател); съзнателен компромис, edge-кеширан, но да се следи с ръста на корпуса → евентуален precompute rollup.
  7. annexEur — NULL валута се третира като BGN; истински EUR ред без валута би бил погрешно делен на PEG. Рискът е приет и описан.
  8. CSS — дребни непоследователности в шрифтовите декларации и повтарящи се em блокове; backdrop-filter без -webkit- префикс (декоративно).
  9. Node engine — sharp@0.35.3 вдига изискването на >=20.9.0 (отпада Node 18); само dev/build-time, но да се потвърди, че CI/локалната среда ползват Node ≥20.9.

Ограничения на прегледа

pages.css (+3080 реда) беше прегледан без наличен diff — не можа да се провери за дублиране/мъртви правила. Финалната консистентност между новите query параметри (angle, step, by, cpvSort, cur), премахването на g от каноничните cache-key параметри и потребителите на cpvGroupSelection зависи от cross-batch drift guard тестовете.

Заключение

Солиден, добре тестван и сигурен PR без блокери. Вердикт: COMMENT — окончателното одобрение зависи от преминаването на тестовете и cross-batch консистентността; за прегледаните файлове няма възражения за сливане.

Comment thread apps/web/app/lib/filters.ts Outdated
Comment thread apps/web/app/routes/trends.tsx Outdated
Comment thread apps/web/app/styles/components.css
Comment thread packages/config/src/index.ts Outdated
# Conflicts:
#	osv-scanner.toml
#	packages/db/src/migrations.test.ts
#	packages/db/src/queries/trend.test.ts
#	pnpm-lock.yaml
#	pnpm-workspace.yaml
MAX_CPV_RAW_VALUES was applied to sp.getAll('cpv').slice(...) before the
comma-split, so a single ?cpv=1,2,3,...,N param bypassed the cap entirely —
slice(0, 50) passed the one param through, then flatMap expanded it to N
values. The cap now applies to the flattened, split array so one long CSV
param is bounded the same as that many repeated params.

Addresses ydimitrof's review on PR midt-bg#171.
The 24 contract-limit and the "(показани първите 24)" label were two
separate literals that could drift apart on a future limit change. Both
now read from a single OVERVIEW_LIMIT constant.

Addresses ydimitrof's review on PR midt-bg#171.
…-title

--font-serif is always defined (tokens.css) and already carries its own
Georgia/serif fallback chain, so the explicit ", Georgia, serif" here was
dead weight and inconsistent with .trend-header-title / .trend-fs-title,
which both use the bare var(--font-serif). Unified on the bare form.

Addresses ydimitrof's review on PR midt-bg#171.
cpvBucket fell through to 'goods' for any catalogued division not in
CPV_BUCKET_WORKS/SERVICES. A future services division added to CPV_SECTORS
but forgotten in CPV_BUCKET_SERVICES would silently misclassify as goods,
with no test able to catch it (the existing partition test only asserted
!== 'other'). CPV_BUCKET_GOODS is now an explicit set; an omitted division
now falls to 'other', and a new test asserts
WORKS ∪ SERVICES ∪ GOODS === CPV_SECTORS exactly.

Same change is required byte-identical on the sibling PRD
feedback-193-methodology branch, which flags the same line — coordinate
before either merges.

Addresses ydimitrof's review on PR midt-bg#171 (also flagged on midt-bg#193).

@ydimitrof ydimitrof left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Преглед на PR: feat(web): overruns dashboard

Какво прави PR-ът

Този PR въвежда табло „Раздувания/Договори — обзор", което преработва предишния изглед „Тренд във времето" в пълноценен аналитичен dashboard. Промяната обхваща:

  • Логика и данни: чисти, SSR-безопасни функции за диаграми и инспектор (overruns-chart.ts, overruns-inspector.ts, cpvGroupSelection), нови DB заявки за раздувания и CPV bucket/division помощници (trend.ts), нормализация на валута към EUR и миграция за индекс.
  • Route/презентация: overruns.tsx и OverrunsDashboard, изцяло навигиращи през обикновени <Link> GET заявки (без JS), с валидирани и ограничени входни параметри.
  • Стилове: значителни промени в components.css и pages.css за таблото, fullscreen модала, popover-и и list-search контрола.

Промяната е чиста, дисциплинирана и много добре покрита с тестове.

Сигурност

Няма злонамерен код в нито един пакет: без мрежови извиквания, eval, обфускация, нови зависимости или промени по auth/CI/permissions. Всички потребителски входове (cpv, angle, step, sort, cpvSort, year, cur, by) са валидирани и ограничени преди да достигнат SQL и edge-cache ключа — това затваря както SQL инжекциите (всичко минава през bind(...)), така и раздуването на кеш-ключове (CWE-349). Валидацията на CPV кодове (/^\d{5}$/), премахването на дубликати и двойните лимити са добро втвърдяване. Целият рендер минава през JSX с авто-escape. Няма следа от prompt-injection.

Коректност и целостност на данни

Математиката в геометрията на диаграмите има честни защити срещу деление на нула, отрицателни дялове и празни корпуси. FX нормализацията към EUR (peg за BGN, идентитет за EUR, null за валута без курс), подът от €1000 и повторните JS проверки предпазват процентите от Infinity/NaN. SQL огледалото SECTOR_KEY_SQL и JS cpvDivision конвергират коректно. Percentile/медиана аритметиката и YoY логиката са проверени и покрити от тестове.

Незадължителни, неблокиращи бележки

  • overruns.tsx (SectorSection): topGrowthCode се инициализира с празен код, а водещият сектор се маркира по s.code; при празен код всички редове с празен код биха получили акцента is-top. Обмислете сравнение по уникален идентификатор.
  • trend.ts (getCpvGroupStats): headline KPI totalGroups брои групи само върху tenders, докато top-N групите идват от contracts JOIN tenders с amount_eur > 0 — възможно е леко разминаване. Уточнете дали семантиката е „групи с харчене" или „всички групи в корпуса".
  • CSS: дублиран селектор (.trend-chart-panel--full .trend-chart-body) в components.css, който е безвреден, но си струва да се обедини.

Точка за внимание преди сливане

pages.css е с промяна +3080/-3, но беше предоставен без patch („no patch available") и не можа да бъде прегледан. Такъв голям, непроверяем diff е необичаен за dashboard и е потенциален канал за скрито съдържание. Препоръчва се ръчна проверка за дублиран/генериран/минимизиран код, случайно committнати build артефакти или вградени base64/data-URI payload-и — и файлът да бъде предоставен като текстов diff за коректен преглед.

Заключение

Няма блокиращи проблеми в прегледаните пакети. Единственото условие преди сливане е ръчна проверка на непрегледания pages.css. С това уточнение PR-ът е одобрен по същество.

Comment thread apps/web/app/lib/overruns-inspector.ts Outdated
Comment thread apps/web/app/routes/trends.tsx
Comment thread apps/web/app/routes/trends.tsx
Comment thread apps/web/app/styles/components.css
Comment thread packages/db/migrations/0011_contracts_overrun_index.sql
- overruns-inspector: round-trip the parsed end date against its own
  year/month/day so semantically impossible dates (2024-13-40,
  2024-01-00) are rejected instead of silently rolled over by Date.UTC.
- trends: scope includeCurrent to the time angle, the only lens with a
  visible toggle for it, so switching to cpv/cross can't silently pull
  in the current period.
- trends: note explicitly that the summary/chart cover the whole
  period while the list is year-scoped, since getSpendingTrend has no
  year param.
- components.css: drop the duplicate `.trend-chart-panel--full
  .trend-chart-body` rule, keeping the fuller (flex: 1; min-height: 0)
  definition.
- 0011 migration: correct the perf comment — corpus_signing_eur scans
  all contracts with no WHERE, so the partial index does not serve it.
apps/web's baseline drops from 91%/82.4% to 85.3%/73.3% lines/branches:
the new trends.loaders.test.ts imports trends.tsx for the first time,
so v8's coverage provider now instruments (and reports on) the whole
route file, including its previously test-invisible JSX component —
not a real regression, just first-time visibility into an existing gap.
Every other workspace's baseline rises to match commits already on
this branch (apps/etl, packages/config, packages/db, packages/ingest).

@ydimitrof ydimitrof left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Обобщен преглед на PR: feat(web): overruns dashboard

Какво прави PR-ът

Този PR добавя ново табло „Раздуване" (overruns) към уеб приложението, заедно със свързаните преработки на страницата „Договори — обзор". Промените включват:

  • Нови чисти, SSR-безопасни помощни функции за графики и inspector (overruns-chart.ts, overruns-inspector.ts), презентационни route-ове (overruns.tsx) и рефакториране на обзорната страница.
  • Заявки само за четене в слоя за данни (overruns.ts, trend.ts) с bind-нати параметри, ограничени лимити и защита срещу деление на нула.
  • CPV partition логика в @sigma/config (cpvDivision/cpvBucket), нови типове в api-contract, forward-only миграция (0011) за overrun индекс и дизайн токени.
  • Разширени cache-ключове и query параметри (cur, by, angle, step, cpvSort), строга валидация на вход (cpvGroupSelection, pick, year regex, cur).
  • Обширни CSS стилове (components.css, pages.css) за layout, fullscreen модал, popover и list-search.
  • Силно тестово покритие, целящо рисковите ръбове (невалидни дати, деление на нула, празни набори, clamping, четен/нечетен n за медиана).

Обща оценка

Като цяло това е чиста, внимателно обмислена и добре структурирана промяна. Логиката е коректно изведена в чисти функции, входните параметри се валидират стриктно, а страниците остават напълно SSR/no-JS. Не е открит злонамерен код, инжекции, XSS, изтичане на данни, неочаквани мрежови заявки, тайни или промени в auth/CI. Няма следи от prompt injection в дифа. Всички стойности се рендерират като екраниран JSX; SQL заявките използват bind-нати параметри.

Незадължителни забележки (не блокират)

  • Устойчивост на заявката за медиани при празен вход в обзорната страница (Batch 3) — струва си дребно уточняване.
  • Спад в базовата линия за покритие на apps/web (Batch 6) — да се потвърди дали е приемлив.
  • authorityEik в overruns.ts/trend.ts (Batch 7) — поведението си струва да се потвърди, но не блокира.
  • CSS дублиране: повтарящ се font: shorthand с 'IBM Plex Mono', var(--font-mono) би могъл да се извлече в обща променлива/клас (Batch 4) — чисто поддръжка.

Изисква ръчна проверка от автора/ревюъра

  • apps/web/app/styles/pages.css (+3080/−3) не беше достъпен за преглед (без patch). Прирастът от над 3080 реда за стилове на едно табло е необичайно голям. Препоръчва се ръчна проверка дали целият прираст е реален, ръчно писан CSS (а не случайно закоммитнат минифициран/генериран изход или дублирани правила) и дали няма вградени url(...)/@import към външни домейни или base64/data-URI payload-и. Това не е потвърден дефект, а непрегледано съдържание — за пълноценен преглед файлът е добре да бъде предоставен като текстов diff или разделен на по-малки части.

Заключение

Няма блокиращи концерни в прегледаните партиди. Всички седем партиди са одобрени, с единствено предупреждение да се извърши ръчна проверка на непрегледания pages.css, преди сливане.

Comment thread apps/web/app/lib/filters.ts
Comment thread apps/web/app/components/FullscreenButton.tsx
Comment thread apps/web/app/routes/trends.tsx
Comment thread coverage-baseline.json Outdated
Comment thread packages/db/src/queries/overruns.ts Outdated
StanislavBG and others added 5 commits September 19, 2026 23:05
Renumber contracts_overrun_index 0011 -> 0023 (byte-identical with pr/dash-base),
take main's coverage baseline and lockfile, align shared query-param/pages.css hunks.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rop unread g param

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…gates, chart ticks and cpv bucket fallback

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ricInfo and fullscreen hook

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ра преди cap; fullscreen за наследник

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
StanislavBG added a commit to StanislavBG/sigma-pr that referenced this pull request Sep 20, 2026
… номериране на миграциите, coverage baseline, споделени резолюции

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants