feat(web): overruns dashboard#171
Conversation
nedda76
left a comment
There was a problem hiding this comment.
Преглед на 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-ва за това); бих помолила да отпадне.
lyubomir-bozhinov
left a comment
There was a problem hiding this comment.
Пуснах целия 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 по същество.
|
Благодаря за щателния преглед — всичко адресирано (force-push 🔴 Анекс хронология: |
lyubomir-bozhinov
left a comment
There was a problem hiding this comment.
Върнах се на този след 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 кода реално има, но е дефанзивно.
|
Отразено в cdfe174:
|
|
Perf доуточнение върху предишния fix ( |
|
По молбата —
Approve; merge след #170. Same 0002-миграция бележка като на #170 — преномерирай на |
|
Преглед на PR #171 — „overruns dashboard" ( Сигурност / SQL (OWASP A03 — Injection)Прегледах всяка заявка в
Цялост на даннитеПотвърждавам наблюденията на колегите: €1000 подов праг двойно защитен (WHERE + Забележки (не блокери)
Обхватът е чист (само overruns файловете спрямо ВЕРДИКТ: APPROVE (по същество) — при условие че миграцията се преномерира на |
|
Rebase-нат върху main с css split (ov-* → styles/pages.css; 0002_contracts_overrun_index остава валиден номер), prettier-чист, линеен. Старите нишки резолвнати — всички находки бяха адресирани и препотвърдени в последвалите прегледи. |
|
Добавени са ⓘ дефиниции на колоните в двете таблици („Кои институции раздуват най-много" и „Раздуване по сектори") — какво означава метриката и как се смята (растежът е претеглен по €, не средно на процентите; включени са само договори с анекс, текуща > подписана и подписана ≥ 1000 €). Плюс дишащо поле вдясно в редовете на „Договори по мащаб". Нов head: 52687a6. |
108d095 to
b99bb14
Compare
Преглед на PR #171 — „overruns dashboard" (
|
Преглед на PR #171 — „overruns dashboard" (
|
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.
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.
|
@todorkolev готов за ревю 🙏 — rebase-нат на main, CI зелен, prettier-чист, CSS промените в styles/* (app.css само @import). Резолвнати нишки. Approve-ни когато ти е удобно. |
|
Одобрявам. overruns loader-ът чете само |
…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.
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
routes/overruns.tsx(+routes.tsline).overruns-chart.ts,overruns-inspector.ts.packages/db/src/queries/overruns.ts—getOverrunsAnalytics,getOverrunAnnexes,getOverrunsHeadline; barrel-exported fromqueries/index.ts.packages/config/src/index.ts—cpvBucket(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
maindue 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.