Skip to content

feat(web): overruns dashboard#171

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

feat(web): overruns dashboard#171
StanislavBG wants to merge 19 commits into
midt-bg:mainfrom
StanislavBG:pr/overruns

Conversation

@StanislavBG

@StanislavBG StanislavBG commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Overruns dashboard

The /overruns page: contract cost-overrun analytics (annex-driven value growth) with a sortable inspector, works/goods/services bucketing, and authority/sector breakdowns. Builds on Trends (PR 2).

Key changes

  • Route: routes/overruns.tsx (+ routes.ts line).
  • Libs (+ tests): overruns-chart.ts, overruns-inspector.ts.
  • Query (+ test): packages/db/src/queries/overruns.tsgetOverrunsAnalytics, getOverrunAnnexes, getOverrunsHeadline; barrel-exported from queries/index.ts.
  • Config (+ test): packages/config/src/index.tscpvBucket (CPV division → works/goods/services, per Directive 2014/24/EU).

Stack

PR 3 of 4. Depends on pr/trends (PR 2). Merge order: dash-base → trends → overruns → analyze. Targets main due to cross-fork stacking; review its diff against pr/trends.

Reviewed: 2 swarm rounds + adversarial verification + data-layer + UX passes; 287 tests, production build green.

@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_LABELSsectorRef; един sectorLabel helper; sortHrefwithParams.
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

Преглед на 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

Преглед на 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

Преглед на 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).

The popover often renders inside a thead th whose white-space: nowrap is
inherited by every line of the card, so long summaries and the mono
readout ran past the right edge. Reset wrapping on .metric-info-pop
(white-space: normal + overflow-wrap: anywhere), widen the card to
320px clamped to the viewport (min(320px, 100vw - 16px)), and add the
JS shift-into-viewport + coarse-pointer 44px hit area so all copies of
the component behave identically.
The обзор cross lens now re-runs the combo chart, year cards and totals
server-side for the ticked CPV groups (repeatable ?cpv, validated
5-digit codes, deduped, capped at 10), and the chart stretches to fill
its card against the CPV list. One aggregate scan via an OR of half-open
cpv_code index ranges; ?cpv stays keyed in the edge cache (CWE-349) with
UI-canonicalized ordering, and selection changes are announced via an
sr-only status line.
The by-sector aggregate grouped on substr(t.cpv_code, 1, 2) and then ran
cpvDivision() over the already-truncated prefix, so a dirty code like
' 45000000' resolved to division '4' there while the leaderboard
(cpvDivision over the full code) resolved it to 45 — the same contract
landed in different CPV sectors on the two surfaces.

Select the full t.cpv_code, GROUP BY it in SQL, and run cpvDivision()
on the full code in JS exactly like the leaderboard mapping; the
existing merge folds all codes onto their canonical division and
re-applies secLimit post-merge. Test covers a dirty leading char
flowing through both surfaces to the same division.
…he full cpv_code

Grouping by the full cpv_code fixed the dirty-code divergence but returned
one row per distinct 8-digit code — 582 rows on the local corpus (thousands
at scale) shipped out of D1 for a 15-row table. SECTOR_KEY_SQL strips the
separator characters real codes carry and takes substr(clean, 1, 2) only
when the cleaned code provably starts with two digits (in which case it IS
cpvDivision(code)); anything else falls through as the full raw code for
the JS re-key to fold in. Exact cpvDivision semantics, division-sized
result set, still no pre-merge LIMIT truncation.

Adds a sqlite3-CLI integration test pinning SECTOR_KEY_SQL ≡ cpvDivision
across the dirt corpus, and a unit test pinning the GROUP BY key against
both traps (naive substr, full-code blowup).
Every column header in „Кои институции раздуват най-много" and „Раздуване
по сектори" now carries the same ⓘ MetricInfo popover the headline KPIs
use: what the metric is, how it is computed (grounded in the SQL — growth
is SUM(delta)/SUM(signing), €-weighted, not an average of percents), and
the honest inclusion caveat (annex_count > 0, current > signing,
signing ≥ €1 000). Table header cells get white-space: nowrap so the ⓘ
never wraps the label.

Also: the „Договори по мащаб" leaderboard rows gain right-side breathing
room (row padding-right var(--s-3)) so value text and truncated titles no
longer touch the card edge; the inset lives on the row, so the selected
highlight still reads full-width and the proportional bar-track math is
untouched in normal and fullscreen mode.
StanislavBG added a commit to StanislavBG/sigma-pr that referenced this pull request Jul 3, 2026
A fresh `wrangler d1 migrations apply` hit "duplicate column": the nine
health-index columns lived both in 0000_init.sql and as ADD COLUMNs in the
health migration. SQLite has no ADD COLUMN IF NOT EXISTS, so the columns now
live ONLY in the migration, removed from 0000_init; the health rollup tables
stay in 0000_init (and are rebuilt idempotently by the ETL derives for
already-migrated DBs). The migration is renumbered 0002 -> 0003 to leave 0002
to 0002_contracts_overrun_index (PRs midt-bg#170/midt-bg#171).

0000_init's second role (direct schema load for the work-DB backfill and the
sqlite-backed tests) is preserved by applying the FULL migration chain there
too: scripts/import.mjs and the db test fixtures now read every migration in
apply order, exactly like a fresh D1. migrations.test.ts applies the full
fresh chain and asserts the columns and rollup tables exist — the regression
test for this blocker.
@StanislavBG

Copy link
Copy Markdown
Contributor Author

@todorkolev готов за ревю 🙏 — rebase-нат на main, CI зелен, prettier-чист, CSS промените в styles/* (app.css само @import). Резолвнати нишки. Approve-ни когато ти е удобно.

@lyubomir-bozhinov

Copy link
Copy Markdown
Collaborator

Одобрявам. overruns loader-ът чете само by (line 49) — деклариран е в allow-list-а, няма cache drift; SECTOR_KEY партишън-индексът е на място. Чисто.

…ontainer

It was the only wide table on /overruns missing .ov-table-scroll (the institutions table right
below it already has it), so on narrow screens it broke out of the panel and spilled past the
right edge of the viewport instead of scrolling contained.
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