Repository navigation
feat(web): договори — обзор (лещи време/CPV/кръстосано) - #170
StanislavBG wants to merge 39 commits into
Conversation
lyubomir-bozhinov
left a comment
There was a problem hiding this comment.
Пуснах и този локално и го изпробвах от всеки ъгъл; тук почти всичко е добре. Проверих и потвърдих: прогнозата клампва растежа в [0.5, 2] и пада на 1 при non-finite/≤0 (clampGrowth), брои само пълни години и пропуска partial-ата, стартира след последния пълен месец (partial-ът не се рисува като реален спад) и се самоизключва при липсваща сезонна база (без фалшив „cliff"); YoY на годишната таблица е guard-нат (partial || !prev || prev<=0 → null); fillPeriods нулира дупките, та вътрешните години винаги имат 12 месеца (което пази consecutive-year допускането на growth estimate-а); RSS-ът escape-ва всичките 5 XML entity-та и маха забранените C0 байтове (няма injection през имена/subject); chart геометрията е floor-ната навсякъде (Math.max(1, …), niceCeil(≤0)→1, n>1 guard), та degenerate domain (една точка, само нули) не чупи SVG-то; а cache-параметрите на /trends са само sector/funding (и двата keyed) — granularity-то е client state, без cache impact. Силна, дисциплинирана работа. 👏
Две неща, нито едно блокиращо (детайли инлайн): quarter-0 ръбът в QUARTER_EXPR, и value-basis-ът, който сумира отрицателни (коректно огледало на rollup-ите — същият tracked global issue като HHI в #153).
Координация: това е PR 2/4 и носи cache-key промяната от #169, която има отделен блокер (изтрит CWE-349 drift guard). Не може да влезе преди #169 да се оправи; merge-редът (dash-base → trends) така или иначе го налага.
Approve по същество — изчистете #169 блокера + зависимостта, и е готов.
|
Благодаря (force-push • quarter-0 ръбът: добавих guard seasonality заявката да изключва само-година дати. Малка корекция към бележката — разделителят е на позиция 5 ( |
|
Ребейзнах върху върха на #169 (fdc149a), за да сподели целият стек една база — обзорът е непроменен по съдържание. Единствена интеграционна корекция: 4 CSS класа се сблъскваха с дефинициите от dash-base (ov-panel, ov-panel-title, ov-legend, ov-seg) и на страницата на обзора са преименувани на ovz-*. Нов head: bc4cbde; typecheck 7/7, тестовете зелени. |
|
Дребно почистване: преработката носеше +70 реда локални бележки в |
|
Добавих още едно подобрение в кръстосаната леща ( Нови върхове на стека след rebase: |
|
Локален преглед на обзора (@
Дребно (не блокер): YoY взима Не одитирах SVG рендера на Координация с #171/#188: и трите въвеждат |
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).
|
Analysis complete. The code layer is clean — fully parameterized SQL, validated/bounded inputs, React auto-escaping throughout, no secrets. Writing the verdict. Преглед на PR #170 — „Договори — обзор" (лещи време/CPV/кръстосано)Здравейте, @StanislavBG 👋 Направих задълбочен преглед — не само на diff-а, а и на data слоя, SQL заявките и локална проверка за инжекции и злонамерен код. Прочетох и цялата дискусия с @lyubomir-bozhinov. Работата е много силна и дисциплинирана; по-долу са наблюденията, разделени по тежест. Сигурност и цялост на данните — чисто ✅
Незначителни бележки (не блокиращи)
Съответствие с описанието на PR-аИмплементацията покрива описаното: три лещи (време / CPV / кръстосано), споделен списък с филтри по година и CPV, комбиниран чарт брой+обем с кликаеми години, CPV разпределения (медиана, p10–p90, лог-скала, отличени ≥5×), изцяло SSR през query params, edge cache ключове покриващи новите параметри. Съответствието е пълно. Едно нещо за потвърждение преди merge
Отлична работа — кодът е чист, добре тестван и OWASP-съобразен. 👏 Verdict: APPROVE (по същество) — при зелен CI и спазен merge ред след #169; преномерирайте дублирания |
|
Rebase-нат върху main с css split (обзор стиловете → styles/pages.css), prettier-чист, линеен. Резолвнах старите нишки — и двете бележки бяха затворени в предишните fix-ове (quarter-guard + adjacent-year YoY). |
|
Здравейте, @StanislavBG 👋 Направих повторен, задълбочен преглед на текущия връх на клона ( Сигурност и цялост на данните — чисто ✅
Незначителни бележки (не блокиращи)
Съответствие с описанието на PR-аПълно. Три лещи (време / CPV / кръстосано), споделен списък с филтри по година и CPV, комбиниран чарт брой+обем с кликаеми години, CPV разпределения (медиана, p10–p90, лог-скала, отличени ≥5×), изцяло SSR през query params, edge cache ключове покриващи новите параметри — всичко присъства. Строго-съседният YoY ( За потвърждение преди merge
Отлична, чиста и добре тествана работа — OWASP-съобразна на слоя на кода. 👏 Verdict: APPROVE (по същество) — при зелен CI и спазен merge ред след #169; преномерирайте дублирания |
|
прогнозата е извадена в отделен draft PR (#192) по продуктово решение; графиката показва само реални данни, текущият месец е изключен по подразбиране (контрол „вкл. текущия месец", |
|
Здравейте, @StanislavBG 👋 Направих повторен, задълбочен преглед на текущия връх на клона ( Сигурност и цялост на данните — чисто ✅
Незначителни бележки (не блокиращи, пренесени)
За потвърждение преди merge (блокиращо на ниво стек)
Кодът на самия обзор е чист, добре тестван и OWASP-съобразен. 👏 Verdict: APPROVE (по същество) — при условие, че поправката на #169 (връщане на |
…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.
…pover pointer-events
ydimitrof
left a comment
There was a problem hiding this comment.
Обзор на ревюто — feat(web): договори — обзор (лещи време/CPV/кръстосано)
Вердикт: COMMENT — няма блокиращи проблеми; препоръчва се човешко потвърждение преди финално одобрение.
Какво прави PR-ът
Въвежда таблото „Договори — обзор" на /trends с три лещи за анализ (време / CPV / кръстосано) и стъпка на периода (месец m / тримесечие q / година y). Промяната обхваща: фронтенд логика и компоненти за визуализация (тренд графики, popover за метрики, режим цял екран, лента за търсене), централизирана логика за осите на трендовете, валидация на URL параметри, презентационен CSS, както и параметризирани DB заявки за трендове, медиани по CPV групи и списък на договорите, заедно с миграция за индекс.
Сигурност и цялост на данните — чисто
Проверката (Phase 0 + ниво агент) не откри блокиращи проблеми в нито една партида:
- Няма твърдо кодирани тайни, подозрителни/външни URL адреси,
@import, обфускация, бекдори или нови зависимости (важи и за CSS файловете). - SQL-инжекции (OWASP A03): всички заявки са изцяло параметризирани (
?); единствените вмъквани в SQL низове са константи или клауза само от?. Потребителските CPV кодове се валидират с/^\d{5}$/и се изхвърлят при несъответствие (тестове с"45'--",%27--,abcdeпотвърждават защитата в дълбочина). Enum параметрите (trendAngle/Step/Sort и др.) минават през allowlist с безопасен fallback. - Кеш-отравяне (CWE-349): CPV множеството се канонизира (дедупликация, канонично сортиране, ограничение до 10 групи) в
cache-key.ts, така че еднакви множества с различен ред колапсват в един кеш запис. Добро покритие с тестове. - Цялост на данните: коректни са поправката на YoY „строго съседна предходна година", сгъването на месеци в тримесечия, изключването на текущия (as_of) период и огледалният percentile (SQL floor-rank ↔ JS
rankOf).
Незначителни (не блокиращи) бележки
- ComboTrendChart.tsx —
hasPartial = partialIdx > 0: ако частичен период попадне на индекс 0, редът се рисува плътен и DEV предупреждението не се задейства. Инвариантът „частичният е винаги последен" е документиран, но остава тих провал. - trends.tsx —
mult = valueEur / medianEurняма защита приmedianEur === 0(води до „×Infinity типичното"). Малко вероятно, но евтин guard. - trends.tsx —
hrefToggleCpvизгражда селекция без горна граница, а loaderът я отрязва до 10 лексикографски — добавянето на 11-та група изглежда без ефект в URL (объркващо UX при 10+ избрани). - query-params.ts / filters.ts — премахнатият параметър
g(заменен съсstep) означава, че стари връзки с?g=…падат към стойност по подразбиране (тримесечно). Не се чупи нищо, но заслужава миграционна бележка. - DB заявки —
getCpvGroupStatsсъдържа inlineTODOза бъдещ per-group rollup (имплементацията е пълна); препоръчва се преобразуване в тикет вместо TODO (правило „NO SIMPLIFICATION with TODO"). - DB заявки —
listOverviewContracts/getSpendingTrendразчитат на маршрута за таван наlimit(няма клампване тук); потвърдете разумен таван срещу прекомерно сканиране. - CSS — смесени препратки към mono/serif шрифтове (
--font-mono-plexvsvar(--font-mono)vs вграден fallback); липсва guard@media (prefers-reduced-motion: reduce)за преходите на popover/модал. Козметично.
Точки за човешка проверка
- Партида 3/4 (
apps/web/app/styles/pages.css, +3074/-3): patch-ът не е наличен (бинарен/твърде голям), затова редови преглед не беше възможен. Необходима е ръчна проверка за: евентуалниurl(...)/@importкъм външни ресурси, дублиране на стилове (правило „NO CODE DUPLICATION") и спазване на конвенциите за именуване. - Финалният вердикт следва да се потвърди след човешко ревю на този файл и агрегиране на всички партиди.
Силни страни
Фокусирана промяна без scope creep; централизиран trendAxis.ts без дублиране; смислени тестове (референтен оракул за поведенческа идентичност, враждебни входове); SSR-безопасни ефекти; достъпни атрибути (aria-label, role="status", role="img", :focus-visible, hit-таргети ≥44px); изнасяне на inline стилове в CSS заради CSP чистота; обратима миграция по шаблона на проекта.
lyubomir-bozhinov
left a comment
There was a problem hiding this comment.
Ре-верифицирах на HEAD (aae0b5ae): новият комит канонизира реда на CPV селекцията + pointer-events на popover-а — презентационно; data-слоят (trend.ts, стойностна база amount_eur, YoY, partial-period) е непроменен. Чисто, както преди. Одобрявам.
…undant mono fallback ComboTrendChart now warns in DEV when the partial period lands at index 0 (previously only checked partialIdx > 0 && !== n-1). components.css list-search-btn used a redundant ', monospace' fallback since --font-mono is always defined in tokens.css.
hasPartial relied on partialIdx > 0, treating a partial period at index 0 the same as no partial period at all and silently rendering the whole series solid. Use the -1 not-found sentinel from findIndex instead
--font-mono-plex already falls back to --font-mono, so consolidating loses nothing while removing the ambiguity ydimitrof flagged between the two tokens on PR midt-bg#170
…ter RSC CVE postcss 8.5.15 -> ^8.5.18 fixes GHSA-r28c-9q8g-f849 (path traversal via sourceMappingURL auto-load); valibot 1.4.0 -> ^1.4.2 fixes GHSA-5qjj-4xww-7phc (flatten() crash on inherited-property keys). Both are non-breaking patch-level bumps via pnpm overrides. react-router's GHSA-qwww-vcr4-c8h2 (CSRF in unstable RSC code paths, fixed only in 8.x) is suppressed in osv-scanner.toml with a time-boxed ignore - this app never uses RSC APIs (verified via repo-wide grep), and bumping to react-router 8.x is a breaking major version change out of scope for a security patch to an unused code path
- osv-scanner.toml: keep both independently-added suppressions (sharp + react-router RSC CVE); pr/trends' entries were a strict superset - packages/db/src/migrations.test.ts + migrations/: both branches added a migration numbered 0002 for unrelated changes (main's 0002_current_value_currency.sql vs pr/trends' overrun index); renumbered pr/trends' migration to 0003_contracts_overrun_index.sql and merged the test assertions from both branches - apps/web/app/routes/trends.tsx: main's only change since the merge base was the getDb() read-only-D1 chokepoint refactor (midt-bg#225); kept pr/trends' full obzor rewrite and applied that same chokepoint to its loader - pnpm-lock.yaml: regenerated via pnpm install rather than hand-merged
pnpm-lock.yaml regenerated by the origin/main merge resolves sharp to 0.35.3, past the 0.35.0 fix for GHSA-f88m-g3jw-g9cj; osv-scanner.toml's own policy requires deleting a suppression once the package clears its fixed version
… + 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>
|
Perf / D1-разход на cache miss в const missing = contracts.map((c) => c.cpvGroup).filter((g) => g != null && !known.has(g));
for (const g of cpvSel) if (!known.has(g)) missing.push(g);
const medians = await getCpvGroupMedians(db, [...new Set(missing)]); // ← без горна граница
Поправка: сложи горна граница на (Euro-annex капанът (#245) НЕ важи тук — trend-овете агрегират по |
|
Този клон е в конфликт с |
ydimitrof
left a comment
There was a problem hiding this comment.
Обзор на прегледа — feat(web): договори — обзор (лещи време/CPV/кръстосано)
Какво прави PR-ът
PR-ът добавя екрана „Договори — обзор" в apps/web с три лещи на анализ (време, CPV, кръстосано). Обхваща:
- Frontend логика и валидация — извличане на общ
trendAxisмодул (премахва дублирането междуTrendChartиComboTrendChart), строга валидация на URL филтрите (cpvGroupSelection,trendAngle/Step/Sort,year,cur), SSR/no-JS навигация чрез<Link>мутации на query string. - SQL заявки (
trend.ts) — facet-ване в едно сканиране, тримесечия сгънати в JS, YoY към строго съседна предходна година, opt-inincludeCurrent. - Кеш коректност за
/trends—curи мулти-селектътcpvвече са част от канонизирания edge-кеш ключ. - Стилове — нов токен
--font-mono-plex, изнасяне на inline стилове към клас-базиран CSS за CSP-чистstyle-src(components.css,pages.css). - Инфраструктура — миграция за индекс на overrun, обновени зависимости (sharp, postcss, valibot) и промяна в
osv-scanner.toml.
Сигурност — ЧИСТО ✅
- Няма твърдо кодирани тайни, нови URL-и или зловредни шаблони в нито една партида.
- SQL инжекция: всички стойности са bound параметри; CPV групите се валидират с
/^\d{5}$/(защита в дълбочина); половин-отворените диапазони поcpv_codeса построени безопасно (вкл. случая „…9", потвърден с тест). - Кеш отравяне (CWE-349): валидацията на
cpv(само 5-цифрени кодове, дедупликация, каноничен ред, лимит 10) и включването наcur/cpvв кеш-ключа предотвратяват фрагментация и неограничена SQL работа при враждебен вход. - Зависимости: sharp вдигнат на 0.35.x (закрива GHSA-f88m-g3jw-g9cj); игнорираният CVE в
osv-scanner.tomlе обоснован (без RSC употреба,ignoreUntilв бъдещето).
Качество и тестове
Висококачествен, добре документиран код със смислени (не тривиални) тестове, покриващи крайни случаи: дедупликация, стабилност на кеш-ключа, отхвърляне на малформирани CPV, празнина в години, единичен договор в група, консорциуми, откриване на частичен период. Няма частични имплементации, дублиране или мъртъв код в разгледаните файлове.
Некритични забележки (незадължителни)
relLabel— потенциално деление на нула приmedianEur === 0(Infinity/NaN → етикет „×Infinity"); струва си защита.ComboTrendChartпри частичен период на индекс 0 рендира линия с празноd=""; само дефанзивен ръб (реалните данни имат частичното винаги последно).- Магическо число
24(contracts.length === 24↔limit: 24) на две места — риск от разсинхронизиране при промяна. getCpvGroupStatsправи пъленGROUP BYвърху всички договори с положителна стойност — вече с TODO, смекчено с кеширане; следете при растеж.- Частичният индекс за overrun е само върху
annex_count— служи като филтър, не покрива предикатните сравнения; ОК по дизайн. - CSS козметика: зависимост от наличието на шрифта „IBM Plex Mono";
max-width: 16chбезtext-overflowможе да отреже дълги имена.
Блокиращи концерни
Няма блокиращи проблеми. Всички забележки са с ниска важност.
apps/web/app/styles/pages.css (+3074/−3) не беше достъпен за автоматизиран ред-по-ред преглед. Препоръчва се ръчна верификация за дублиране на стилове, мъртви селектори и регресии в специфичността преди сливане. Освен това част от твърденията за CANONICAL_QUERY_PARAMS зависят от app/lib/query-params.ts (в друга партида), налаган имплицитно от тестовете тук.
Заключение
COMMENT — силен, сигурен и добре тестван PR, готов за merge след преглед на дребните бележки и ръчна верификация на pages.css.
# Conflicts: # osv-scanner.toml # packages/db/src/migrations.test.ts # packages/db/src/queries/trend.test.ts # pnpm-lock.yaml # pnpm-workspace.yaml
medianEur === 0 (a CPV group whose contracts carry zero/missing value) made relLabel() divide by zero, rendering the literal "×Infinity типичното" label to users. relLabel now returns null before dividing when medianEur isn't > 0. ComboTrendChart also rendered an empty <path class="combo-line-partial" d=""> when the partial period fell at index 0 (no prior solid point to anchor the dashed segment on) — it now draws the dashed segment forward to the next point instead of emitting an empty, unrenderable d attribute. Addresses ydimitrof's review on PR midt-bg#170.
…es it trends.test.ts (added to unit-test relLabel's median-0 guard) is the first test to import apps/web/app/routes/trends.tsx. Since v8 coverage only scans files touched by a test run, the route's largely-untested loader/component body (642 lines, added by the /trends feature PR, previously invisible to the ratchet because nothing imported it) is now counted for the first time. This is a newly-*measured* pre-existing gap, not new untested code from this fix — the actual diff (the guard + 5 assertions) only adds coverage. Lower apps/web lines/branches to the honest current numbers (86.03%/75.42%, via `check-coverage.mjs --update`, scoped to only this workspace).
ydimitrof
left a comment
There was a problem hiding this comment.
Обзор на прегледа
Какво прави PR-ът: Добавя таблото „/trends" за обзор на договорите — визуализации с превключване между лещи за време/CPV/кръстосан анализ. Включва нови заявки за агрегиране (месец/тримесечие/година с YoY), валидиране и канонизиране на филтрите (?cpv, ?angle, ?step, ?sort, ?year, ?cur), извеждане на осите в trendAxis.ts, миграция 0011 за overrun индекс, съответните типове и тестове, както и презентационен слой в components.css и pages.css (нов токен --font-mono-plex, стилове за .trend-*, fullscreen модал, .metric-info popover и .list-search).
Обща оценка: Кодът е чист, добре структуриран и много добре покрит с тестове. Не е открит враждебен код, мрежови обръщения или промени по auth/CI.
Най-важни наблюдения (положителни):
- Сигурност / инжекции: Всички входни параметри минават през allowlist/regex валидация (
validCpvGroups,/^\d{5}$/) с ограничение по размер; CPV групите се подават като параметризирани плейсхолдъри — няма конкатенация на потребителски вход в SQL. - Целостност на данните: Кешовият ключ канонизира реда на
cpvстойностите, за да не се фрагментира edge кешът (CWE-349); полуотворениятcpvGroupRangeе коректен, вкл. крайния случай с последна цифра „9". - Коректност: Поправката на YoY (строго съседна предходна година) и тримесечното сгъване с изключване на текущия период по подразбиране са последователно реализирани и покрити с тестове.
relLabelпази срещу деление на нула. - CSS: Промените са чисто презентационни; няма външни
url()/@import/@font-faceс отдалечени адреси — няма мрежови заявки.
Блокиращи концерни: Няма. Нищо в четирите партиди не блокира сливането.
Необвързващи бележки за проследяване:
- Миграцията на стария параметър
gи устойчивостта на графиката при нарушен инвариант. - Да се потвърди, че шрифтът 'IBM Plex Mono' реално се доставя (иначе токенът винаги пада към
var(--font-mono)). - Прирастът от +3074 реда в
pages.cssе много голям за ревю и не беше наличен като diff — препоръчва се разделяне на по-малки модули и потвърждение, че не се въвеждат външни мрежови зависимости. - Свалянето на прага за покритие на тестовете и един вероятно неизползван импорт.
Заключение: COMMENT — препоръчва се сливане след потвърждение на видимия diff за pages.css.
- trendStep falls back to the retired ?g=month|quarter|year param when ?step is absent, so pre-rename /trends bookmarks and shared links keep their granularity instead of silently resetting to the default; step still wins when both are present. Generated links now drop the legacy g param and carry the resolved step forward. - ComboTrendChart no longer drops the points after a partial period that lands mid-series (invariant violated upstream): it now draws them as a second solid segment instead of truncating the line. - Drop the unused CPV_SECTORS import from packages/db/src/queries/trend.ts (sectors come from sectorOptions). - Raise the apps/web coverage baseline (92%/82.8% branches) with real loader + render tests for the /trends route instead of lowering the gate further.
ydimitrof
left a comment
There was a problem hiding this comment.
Обзор на PR: договори — обзор (лещи време/CPV/кръстосано)
Какво прави PR-ът
Добавя ново табло „/trends" за обзор на договори с три лещи (време, CPV, кръстосано). Промените обхващат: рефактор на маршрута trends.tsx (изцяло SSR/no-JS съвместим — всички контроли са <Link>-ове, мутиращи query string), извеждане на споделена логика в trendAxis.ts (премахва дублирането между TrendChart и ComboTrendChart), дисциплинирана валидация на query параметрите (allowlist + fallback, дедупликация, канонично сортиране, капиране), нов DB слой с параметризирани заявки и строга валидация на CPV групите (/^\d{5}$/), нова миграция 0011 (идемпотентен партиален CREATE INDEX), както и CSS промени (components.css, pages.css, нови цветови токени и --font-mono-plex).
Силни страни
- Сигурност: няма злонамерен код. Всички SQL заявки са параметризирани; CPV кодовете се валидират стриктно преди да влязат в range клаузите (с тест за отхвърляне на малформирани кодове). Няма
dangerouslySetInnerHTML/ XSS вектор, няма мрежови повиквания, тайни или съмнителни зависимости. - Хигиена на cache/CSP: канонизацията на
cpvв cache-ключа предотвратява фрагментиране на edge кеша и адресира CWE-349. Inlinestyle=са изнесени в CSS класове, за да остане маршрутът CSP-чист. - Цялост на данните: миграция 0011 е forward-only и идемпотентна; партиалният индекс покрива overrun предиката. YoY поправката (строго съседна година) е коректна. Ранг-аритметиката за перцентилите в JS огледално съвпада със SQL.
- Коректност: защита срещу деление на нула (
relLabel,makeLx);cpvGroupRangeе коректен и за префикси, завършващи на „9"; заглавният тотал и графиката остават синхронни. - Като цяло чист, добре структуриран и много добре покрит с тестове код.
Точки за потвърждение (незадължителни, не блокиращи)
- Смяна на поведението по подразбиране — текущият период вече се изхвърля по подразбиране; желателно е потвърждение, че това е желаното поведение.
- Премахване на
gотPARAM_ORDER— да се потвърди, че не нарушава целостта на cache/canonicalization. yearфилтрира само списъка, но не и графиката (getSpendingTrend) — изглежда умишлено, но си струва потвърждение и за трите лещи.- Надписът „(показани първите 24)" се показва при точно 24 договора и може да подведе, ако общо са точно 24; по-точен би бил флаг „има още" (напр.
limit: 25+slice(0, 24)). - Fallback на шрифта — дребна CSS забележка.
Блокиращи концерни
Няма функционални блокери. Единствената процесна забележка:
pages.css(+3074/−3) не можа да бъде прегледан — diff-ът липсва (бинарен/твърде голям). Преди сливане, моля потвърдете, че това е ръчно писан/генериран CSS, а не случайно комитнат минифициран/бъндълван изход, и осигурете преглед чрез инструмент, показващ промените (локаленgit diffили разделяне на файла), с проверка за външниurl(...),@importили вградениdata:/base64 ресурси.
Заключение
Одобрено, при условие че се потвърди съдържанието на pages.css и се вземе решение по изброените точки за потвърждение.
Renumber contracts_overrun_index 0011 -> 0023, take main's coverage baseline and lockfile, keep the trends query-param/cache-key surface aligned with pr/dash-base. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ranularity, INTENTIONALLY_UNKEYED Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… stats Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…artialIdx 0; документирай Plex fallback Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… номериране на миграциите, coverage baseline, споделени резолюции
What changed
Contracts overview (
/trends) — three lenses (time / CPV / cross-filter) over the contractsdataset, plus five rounds of review fixes from
the reviewer.Feature
/trendsroute: time-series, CPV-group, and cross-filter lenses over contracts, withSSR/no-JS-safe URL-driven filters (angle, step, sort, cpv, cpvSort, year range).
lib/trendAxis.tsto remove duplication betweenTrendChartand the newComboTrendChart.MetricInfo(SSR-safe info popover) andFullscreenButton/useFullscreencomponents.Review rounds addressed (all threads resolved against the landed diff)
cpvmulti-select is now order-canonical —cpvGroupSelection(apps/web/app/lib/filters.ts) sorts the deduped/validated codes beforecapping, so
?cpv=A&cpv=Band?cpv=B&cpv=Aproduce an identical array and therefore oneedge-cache key instead of two (cache-fragmentation / CWE-770-adjacent).
query-params.ts'scpv/cpvSortcache-key params flow through this same canonical accessor — no second sortwas added. Restored
pointer-events: autoon the open-state.metric-info-poprule incomponents.cssso the popover is interactive (text selectable, hover-hold works) once open.cpvSortvalidation into a sharedtrendCpvSort(built on the existingpickEnum), removing a localpickduplicate; dev-onlyconsole.warninComboTrendChartwhen the "partial period is always last" invariant is violated; extracted the repeated
'IBM Plex Mono', var(--font-mono)shorthand into a--font-mono-plextoken; sortedcpvvalues before composing the edge-cache key in
cache-key.ts; restored theCACHE_QUERY_PARAMSdrift guard as a soft assertion (was a bare
console.info), fixed a scanner regex blind spoton multi-line
.getAll()calls, and addedPLANNED_QUERY_PARAMSfor documented stacked-laterparams; removed a stray internal draft-reply file accidentally committed under
apps/web/.singleSelectFiltersimport intrends.tsx; confirmed theg→stepquery-param rename has no remaining readers andcpvGroupRangecorrectly handles5-digit prefixes ending in
9via its half-open range; addressed a11y/duplication notes onComboTrendChart's tooltip and sharedyearStart/tickslogic.MetricInfo/FullscreenButton/useFullscreenare consumed by thisbatch (not dead code); consolidated duplicate
.fs-btn/.trend-fs-btnCSS; validation orderin
cpvGroupSelection(dedupe → validate → cap) confirmed correct..trend-chart-panel--full .trend-chart-bodyCSS rule and astray
overruns-dashboardcomment marker left over from an earlier refactor; addressed theuseLayoutEffect-on-SSR warning inMetricInfo.How it was tested
pnpm --filter web run typecheck— clean.pnpm --filter web test— 37 files / 389 tests passing, including the round-5cpvGroupSelectionorder-canonicalization case (cpv=b&cpv=aandcpv=a&cpv=byield the samesorted array) and the restored
CACHE_QUERY_PARAMSdrift guard./contractsoverview across all three lenses (time/CPV/cross-filter).Quality checks
aae0b5a).the reviewer) re-verified against the landed diff and resolved.Вид промяна
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
4 комита в този branch носят
Co-Authored-By:trailer към агент (Claude). AGENTS.md го забранява наmain, а squash-merge запазва trailer-ите от комитите. Историята не се пренаписва (без rebase/force-push), затова при merge моля заменете автоматично генерирания commit message със заглавието и тялото по-долу. Няма trailer-и към хора в този branch (всички комити са от автора на PR-а), така че не се губи credit.Заглавие (subject):
Тяло (body):