feat(web): analyze index — five equal analysis cards#172
Conversation
nedda76
left a comment
There was a problem hiding this comment.
Прегледах PR-а (пет равностойни аналитични карти на /analytics + специализирани headline заявки). Кодът е стегнат и добре тестван; по-долу са находките, подредени по тежест. Две са с потребителски ефект (счупена връзка и премахната CI защита), останалите са за съответствие на текст с данни и за дублиране.
Прегледът е автоматизиран; преценете всяка бележка по същество.
|
Благодаря — адресирано (force-push • drift guard: възстановен (в базовия #169). |
lyubomir-bozhinov
left a comment
There was a problem hiding this comment.
Проверих срещу 83083e8: всичките 6 находки на Неда са затворени — CWE-349 drift guard-ът е възстановен (consumed ⊆ allowed, expect(undeclared).toEqual([])), няма линк към несъществуващ /compare, картовият текст за единствена оферта е прецизиран, growthMultiple/sectorOptions са унифицирани. Share-математиката е guard-ната (opaqueHeadline филтрира valueEur > 0, value-weighted, текущата непълна година е изключена). „Конкуренция" картата показва дела за последната пълна година (етикетиран с годината, ред 409) — различен обхват от общата цифра на /competition, не противоречие. Една дребна бележка (не блокер): тази карта всъщност сканира contracts (не rollup, за разлика от описанието в PR-а) — чисто perf. Одобрявам.
|
Благодаря за прегледа — по бележката за „Конкуренция" (contracts vs rollup): • Проверка: нито един rollup не носи |
|
Рестакнат върху новия #171 — старите trends/overruns къмити отпадат от историята. Една реална адаптация към обзора: estimateYoyGrowth е пренесен в lib/analytics-stats (старият lib/trends-forecast вече не съществува) и отпадна includeInsights от getSpendingTrend; иначе diff-ът спрямо pr/overruns показва само analyze файловете. Нов head: 5ee4d0d; typecheck 7/7, тестовете зелени. |
Splits the 3353-line app.css into app.css (import manifest) + 9 focused files under styles/ (tokens, base, chrome, layout, components, tables, home, flow, pages). Verified rule-for-rule equivalent to the previous monolith (525 selectors / 1926 declarations, 0 dropped or altered); app.css is now @import-only. typecheck + test + lint green. Open css-touching PRs (#126, #131, #170-#172) now rebase onto the split (target the styles/ files, not app.css).
|
Прегледах PR #172 стриктно, с акцент върху сигурност и цялост на данните. По-долу е обобщението; прегледът е локален и адверсариален (проверих всяка заявка за инжекции, XSS, CWE-349 дрейф и деление на нула). Сигурност (OWASP)SQL инжекции — няма. Проверих всяка нова/променена заявка в
XSS — няма. Няма CSRF / достъп — n/a. Маршрутите са read-only, edge-кеширани GET loaders над публични данни за обществени поръчки; няма мутации, няма секрети, няма външни fetch-ове, няма четене на CWE-349 (кеш дрейф). Цялост на данните
Съответствие с описаниетоОписанието вече е приведено в съответствие с реалността след прегледа на @nedda76 и @lyubomir-bozhinov: „Конкуренция" картата коректно е обозначена като Незадължителни бележки (не-блокери)
Кодът е чист, добре тестван и без открити уязвимости. Отлична дисциплина по параметризацията и guard-овете. Вердикт: ОДОБРЯВАМ — няма блокери по сигурност или цялост на данните; адресирайте само CI видимостта преди merge. |
|
Rebase-нат върху main с css split (az-* → styles/pages.css), prettier-чист, линеен. Старите нишки резолвнати — шестте находки бяха затворени още на 83083e8 и препотвърдени. |
87546d3 to
ab9c6a2
Compare
|
Прегледах PR #172 стриктно и локално, с приоритет върху сигурността и целостта на данните. За разлика от предходните прегледи този път инсталирах зависимостите в worktree-а и изпълних тестовете и typecheck-а сам — по-долу са резултатите, а не думи на автора. Сигурност (OWASP)A03 Injection — няма. Прочетох ред по ред всяка нова/променена заявка в
A03 XSS — няма. Нула CWE-349 (кеш дрейф) — чисто. Достъп/секрети — n/a. Read-only, edge-кеширани GET loader-и над публични данни; няма мутации, няма Цялост на данните
Съответствие с описаниетоОписанието е приведено в съответствие след предходните прегледи (@nedda76, @lyubomir-bozhinov): „Конкуренция" е коректно обозначена като Локална проверка (резултати)
Незадължителни бележки (не-блокери)
Кодът е чист, дисциплиниран по параметризация и guard-ове, и напълно покрит с тестове, които този път изпълних сам. Не откривам блокери по сигурност или цялост на данните. Вердикт: ОДОБРЯВАМ — няма блокери; тестовете, typecheck-ът и CI са зелени, съответствието с описанието е потвърдено. |
|
Всички проверки минаха. db 238/238, web 375/375, typecheck 7/7. Пиша финалния коментар. Прегледах PR #172 стриктно и локално — с приоритет върху сигурността и целостта на данните. Този път инсталирах зависимостите в worktree-а и изпълних целия тестов пакет, typecheck-а и SQL-инжекционния анализ сам; по-долу са резултатите, а не думи на автора. Прегледът е адверсариален: третирах всяка нова заявка като потенциален вектор за инжекция и всяко деление като потенциален източник на измислена цифра. Сигурност (OWASP)A03 Injection — няма. Прочетох ред по ред всеки интерполиран SQL-фрагмент в
Валидация на входа (защита в дълбочина). Целият URL-вход е валидиран преди заявката: A03 XSS — няма. Нула CWE-349 (кеш дрейф) — чисто. Достъп/секрети — n/a. Read-only, edge-кеширани GET loader-и над публични данни за обществени поръчки; няма мутации, няма Цялост на данните
Съответствие с описаниетоНяма свързан тикет за съпоставка; описанието на PR-а служи като спецификация и имплементацията му отговаря (пет равностойни карти, 7 bounded statements на cold load). Описанието е приведено в съответствие след предходните прегледи (@nedda76, @lyubomir-bozhinov): „Конкуренция" е коректно обозначена като Локална проверка (резултати)
Незадължителни бележки (не-блокери)
Кодът е чист, дисциплиниран по параметризация и guard-ове, и напълно покрит с тестове, които този път изпълних сам. Не откривам блокери по сигурност или цялост на данните, нито следа от зловреден код. Вердикт: ОДОБРЯВАМ — няма блокери по сигурност или цялост на данните; тестовете, typecheck-ът и SQL-инжекционният анализ са зелени. |
…ns, overrun index)
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.
…compare lens (review)
|
@todorkolev готов за ревю 🙏 — rebase-нат на main, CI зелен, prettier-чист, CSS промените в styles/* (app.css само @import). Резолвнати нишки. Approve-ни когато ти е удобно. |
|
Одобрявам на връх
Една бележка (не блокер): Останалото — чисто. |
…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.
Analyze index — five equal analysis cards
The /analytics landing page: five equal-weight analysis cards (Flows, Map/Regions, Trends, Competition, Overruns), each with a lean headline metric served by a dedicated bounded query. Flows and Map read precomputed rollups only (
flow_pairs,authority_totals); the Trends, Competition, and Overruns headlines aggregate overcontracts(Overruns via theidx_contracts_overrunpartial index, Trends/Competition bounded byidx_contracts_signed) — no rollup carries thebids_receivedsplit or the month series, so those cannot be rollup reads today. A per-year competition rollup (value + single-bid value per year) is a candidate follow-up to take the lastcontractsscans off this page. Builds on Overruns (PR 3) — completes the stack.Key changes
routes/analytics.tsx.analytics-stats.ts(+ test),analytics-lenses.ts.flows.ts(getFlowsHeadline),regions.ts(getRegionHeadline),competition.ts(getOpaqueShareByYear); consumesgetOverrunsHeadline/getSpendingTrendfrom earlier layers.Query budget (cold load, per 1800s edge-cache window)
7 statements:
getOverrunsHeadline(1, contracts via partial index) +getFlowsHeadline(1, rollups) +getRegionHeadline(1, rollup) +getSpendingTrendmonth series withincludeSectors:false, includeInsights:false(3: series + coverage + as_of) +getOpaqueShareByYear(1, contracts since 2020, full years only).Stack
PR 4 of 4. Depends on pr/overruns (PR 3). Merge order: dash-base → trends → overruns → analyze (last). Targets
maindue to cross-fork stacking; review its diff against pr/overruns.Reviewed: 2 swarm rounds + adversarial verification + data-layer + UX passes; 287 tests, production build green.