feat(web): overruns dashboard - #171
StanislavBG wants to merge 49 commits into
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" (
|
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
… + 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>
|
Този клон е в конфликт с |
ydimitrof
left a comment
There was a problem hiding this comment.
Преглед на 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-та, мъртъв код или дублиране.
Блокиращи концерни
Няма установени блокиращи проблеми или проблеми със сигурността в нито една партида.
Незадължителни бележки (не блокират)
cpvGroupSelectioncap — суровият cap не покрива CSV-разгъването на един параметър с много стойности през запетая; документираната инвариантност не е напълно точна.formatGrowthFactor— множителят се закръгля до 1 знак, но процентът използва суровата стойност (напр.1,4× (+44%)) — консистентно закръгляне би било по-чисто.- Клавиатурна достъпност на scatter-а —
<circle onClick>са само за мишка; приемливо, защото класацията предоставя дублиращ достъпен начин за избор. is-topоткрояване на топ-сектор — сравнение поs.code; празен CPV код ('') би маркирал всички редове с празен код. Краен случай.- Магическото число
24(и10вgetCpvGroupStats) е дублирано между loader и компонент — да се изнесе в именувана константа, за да не се разсинхронизира надписът. - Производителност —
getOverrunsAnalyticsправи пълно сканиране наcontractsпри cache-miss (нужен за корпусно-широкия знаменател); съзнателен компромис, edge-кеширан, но да се следи с ръста на корпуса → евентуален precompute rollup. annexEur— NULL валута се третира като BGN; истински EUR ред без валута би бил погрешно делен на PEG. Рискът е приет и описан.- CSS — дребни непоследователности в шрифтовите декларации и повтарящи се
emблокове;backdrop-filterбез-webkit-префикс (декоративно). - 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 консистентността; за прегледаните файлове няма възражения за сливане.
# 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
left a comment
There was a problem hiding this comment.
Преглед на 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 KPItotalGroupsброи групи само върху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-ът е одобрен по същество.
- 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
left a comment
There was a problem hiding this comment.
Обобщен преглед на 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,yearregex,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, преди сливане.
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>
… номериране на миграциите, coverage baseline, споделени резолюции
…rd-ът вече го вижда Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
What changed
Overruns dashboard (
/overruns) and the обзор /trends page, plus a full pass of the reviewer's review rounds:d694927— moved the preciseMAX_CPV_GROUP_SELECTIONcap incpvGroupSelection(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— sortedcpvSelin the/trendsloader to matchhrefToggleCpv's own sort, so a reordered?cpv=URL with the same selection collapses onto one canonical edge-cache key.TrendYear.yearis declaredstringin@sigma/api-contract, matching the loader's regex-derivedyear, so they.year === yeartoggle comparison is correct as written.tabIndex={0}+onFocusmirroringonMouseEnter.flatMap/dedup, since superseded by the post-validation cap above.singleSelectFiltersimport fromtrends.tsx(lint fix).0002_contracts_overrun_index.sql(annex_count-only partial is sufficient today; composite alternative noted and deferred).CPV_BUCKET_WORKS/SERVICES⊆CPV_DIVISION_SET) so future drift fails CI instead of silently falling toother.mainin (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 typecheckandpnpm --filter web test— full web suite, includingComboTrendChart,filters.test.tscpv-cap coverage, and cache-key coverage.pnpm --filter db test— full db suite, including the CPV-bucket invariant test./overrunsand/trendsafter the merge to confirm the CSS fix and param canonicalization didn't regress rendering.Quality checks
d694927).Вид промяна
fix— поправка на бъгfeat— нова функционалностdocs— документацияrefactor/perf/style— без промяна в поведениетоtest/ci/build/chore— поддръжкаЧеклист
Co-Authored-By:, вече публикувани, не се пренаписват) — виж „Бележка при merge“ по-долуmidt-bg/sigma:mainpnpm 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):
Тяло (body):