Skip to content

feat(web): dashboard design-system base (MetricInfo, fullscreen, tokens, overrun index) - #169

Open
StanislavBG wants to merge 28 commits into
midt-bg:mainfrom
StanislavBG:pr/dash-base
Open

StanislavBG wants to merge 28 commits into
midt-bg:mainfrom
StanislavBG:pr/dash-base

Conversation

@StanislavBG

@StanislavBG StanislavBG commented Jun 28, 2026 •

Copy link
Copy Markdown
Contributor

What changed

Dashboard design-system base (MetricInfo popover, fullscreen toggle, design tokens, overrun index), now through four rounds of review fixes:

  • MetricInfo popover clamp recomputes from the unshifted rect (idempotent across repeated resize/scroll), re-triggers on hover/focus reveal (not just click), and aria-expanded/Esc now track hover+focus visibility, not just open.
  • MetricInfo: removed the stray aria-expanded from the trigger button — the popover it referenced is purely visual and aria-hidden, and its text is already conveyed via aria-label, so the two were inconsistent.
  • MetricInfo: the capture-phase window scroll listener never calls preventDefault, so it's now registered { passive: true, capture: true } (with matching capture: true on removeEventListener so cleanup still detaches it).
  • useFullscreen (zero call sites) removed as dead code; FullscreenButton stays purely presentational — an intentional staged design-system base whose consumer (wiring active/onToggle + the native Fullscreen API, plus any webkit-prefix fallback decision) lands in a follow-on stacked PR (feat(web): overruns dashboard #171, pr/overruns — see apps/web/app/routes/overruns.tsx).
  • lib/query-params.ts: the new canonical params (a, b, by, cohort, cpv, metric) are added ahead of their consumer routes deliberately as part of this stacked base. The security-critical direction (consumed ⊆ allow-list, CWE-349) stays hard-asserted in cache-key.test.ts; only the reverse stale-entry direction is intentionally softened (see next bullet).
  • Stale CSS comments fixed: the trends-dashboard/overruns-dashboard section-marker typo, the duplicate choropleth comment, and --trend-count-ink (no longer claims "darker" than --trend-count — they're byte-identical; the comment now says so and explains why that's acceptable).
  • Cache key (cache-key.ts) draws its response-affecting param set from the shared CANONICAL_QUERY_PARAMS source of truth (CWE-349, issue Web cache poisoning: /contracts edge-cache key omits a response-affecting query parameter #56); confirmed the cohort repeatable-param case is handled (URLSearchParams iteration preserves every occurrence, not just the first).
  • cache-key.test.ts: the removed hard-fail invariant is now an info-only console.info (stale allow-list entries are harmless — lower cache-hit-rate only, not correctness) with a comment explaining the tradeoff. Kept as console.info-only by explicit decision — no threshold/periodic-review follow-up planned; the hard-asserted direction (consumed ⊆ allow-list) is the one that actually prevents cache collapse.
  • 0002_contracts_overrun_index.sql: composite covering index on (signing_value_eur, current_value_eur) (~4x faster than an annex_count-only index per EXPLAIN QUERY PLAN on a 190k-row fixture). No down-migration, matching this repo's forward-only convention (0000_init.sql, 0001_flow_pairs_bidder_index.sql have none either; wrangler d1 migrations has no rollback primitive) — documented in a migration comment rather than fabricating a DROP INDEX.
  • migrations.test.ts strengthened to assert the index's actual partial predicate and column order, not just its presence.
  • MetricInfo.test.tsx added — covers the open/hover/focus/dismissed state machine, Esc-close, and listener cleanup on unmount.
  • --paper-raised uses oklch(100% 0 0) for token consistency.

How it was tested

  • pnpm vitest run app/components/MetricInfo.test.tsx app/components/metric-info-clamp.test.ts (round 4) — green after the aria-expanded removal and passive-listener change; the one assertion that referenced the removed attribute was updated to check the dismissed-state class instead.
  • pnpm --filter web test — 37 files / 383 tests passed, including the new MetricInfo.test.tsx and the corrected metric-info-clamp.test.ts "both edges clip" fixture.
  • pnpm --filter @sigma/db test — 28 files / 199 tests passed, including the strengthened migrations.test.ts assertion on the overrun index's predicate/column order.
  • tsc --noEmit clean for both apps/web and packages/db.
  • Manual popover exercise across hover/click/keyboard-focus reveal paths at a 320px viewport.

Quality checks

  • CI green on the current head commit.
  • ETL/db migration convention preserved (forward-only, additive, IF NOT EXISTS).

Вид промяна

  • fix — поправка на бъг
  • feat — нова функционалност
  • docs — документация
  • refactor / perf / style — без промяна в поведението
  • test / ci / build / chore — поддръжка

Чеклист

  • Комитите следват conventional commits; не е изпълнено на ниво branch (3 комита с agent Co-Authored-By:, вече публикувани, не се пренаписват) — виж „Бележка при merge“ по-долу
  • PR-ът е с един логически обхват и е от форк към midt-bg/sigma:main
  • pnpm typecheck минава (CI: стъпка Typecheck е зелена)
  • pnpm test минава (CI: стъпка Test е зелена)
  • pnpm lint е чисто (CI: стъпка Lint е зелена)
  • Няма комитнати тайни, .env* или .dev.vars (gitleaks + semgrep са зелени)
  • Документацията в docs/ е обновена — не е приложимо за тази промяна

Бележка при merge

3 комита в този branch носят Co-Authored-By: trailer към агент (Claude). AGENTS.md го забранява на main, а squash-merge запазва trailer-ите от комитите. Историята не се пренаписва (без rebase/force-push), затова при merge моля заменете автоматично генерирания commit message със заглавието и тялото по-долу. Няма trailer-и към хора в този branch (всички комити са от автора на PR-а), така че не се губи credit.

Заглавие (subject):

feat(web): dashboard design-system base (MetricInfo, fullscreen, tokens, overrun index)

Тяло (body):

Dashboard design-system base: MetricInfo popover, fullscreen toggle, design tokens and the overrun index, with four rounds of review fixes.

@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.

Благодаря — основата е добре структурирана: MetricInfo и useFullscreen са чисти (SSR-safe, listener cleanup-ът го има, feature-detected), а миграцията е идемпотентна и partial index-ът върху annex_count > 0 е правилно стесненият набор. Но пуснах PR-а локално и има две неща за оправяне преди merge — и двете минават под зелените тестове.

🔴 Премахнат е CWE-349 drift guard-ът на cache-key. cache-key.test.ts губи consumedQueryParams() + целия describe('CACHE_QUERY_PARAMS drift guard') (тестовете, които статично сканират app/** и падат CI, ако consumed param липсва от allow-list-а), а INTENTIONALLY_UNKEYED е махнат от cache-key.ts. Описанието казва само, че allow-list-ът се разширява — не че guard-ът пада. След това: забравен response-affecting param → два различни изгледа колабират в един cache entry → грешни данни под общ ключ, без CI мрежа. Точно за това стоеше guard-ът (#56), и точно него цитират docs-овете в #143/#149 като жива защита.

Виждам и защо е паднал: добавяш a/b/by/metric за /compare и /overruns, чиито route-ове ги няма още на този branch, та „no stale entries" тестът гърми. Но това е perf-hygiene посоката. Сигурностната посока — consumed ⊆ CACHE_QUERY_PARAMS — трябва да остане. Махни само stale-entries assert-а (или премести добавянето на параметрите в PR-овете, които въвеждат route-овете им) — не целия guard. Иначе следващият забравен param (точно в #170/#171) минава тихо.

🟠 app.css трие стиловете на жив компонент. -178-те реда махат .risk-indicators / .risk-title / .risk-list и не ги връщат под друго име (нула .risk* селектора остават в app.css), но RiskIndicators.tsx още ги ползва и се рендва на routes/contract.tsx — страницата на договора. Резултат: risk-кутията на всеки договор става неоформен текст (без warning фон/бордюр/булети). Или върни правилата, или мигрирай RiskIndicators към новите токени в този PR.

Останалото е чисто. Но и двете горе трябва да се затворят преди merge — а понеже това е PR 1/4, блокира целия stack.

@StanislavBG

Copy link
Copy Markdown
Contributor Author

Благодаря за внимателния преглед — и двата блокера са затворени (force-push 6d3ee42):

🔴 CWE-349 drift guard възстановен. Върнах INTENTIONALLY_UNKEYED в cache-key.ts и consumedQueryParams() + describe('CACHE_QUERY_PARAMS drift guard') в теста — със сигурностната посока consumed ⊆ CACHE_QUERY_PARAMS ∪ INTENTIONALLY_UNKEYED (статичен скан на app/**). Махнах само stale-entries assert-а, точно както предложи. Тестът минава.

🟠 Risk стиловете върнати. .risk-indicators/.risk-title/.risk-list възстановени в app.css от origin/main, та risk-кутията на routes/contract.tsx пак е оформена.

@ydimitrof

Copy link
Copy Markdown
Contributor

Благодаря за бързата и прецизна реакция — пуснах branch-а локално и го прегледах стриктно за сигурност и интегритет на данните; и двата блокера на @lyubomir-bozhinov са наистина затворени.

Какво потвърдих локално (6d3ee42):

  • CWE-349 / cache poisoning (Web cache poisoning: /contracts edge-cache key omits a response-affecting query parameter #56): drift guard-ът е върнат и работи реално — статично сканира app/** и налага consumed ⊆ CACHE_QUERY_PARAMS ∪ INTENTIONALLY_UNKEYED. Тестовете минават (8/8) и нито един param, който реално се чете на този branch, не липсва от allow-list-а. Новите a/b/by/metric/cohort/cpv още не се консумират от никой route тук, така че са безвредно предварително добавяне (в най-лошия случай — леко по-нисък cache-hit, никога грешно тяло на отговора).
  • Risk стилове: .risk-indicators/.risk-title/.risk-list са възстановени и страницата на договора пак е оформена.
  • Injection / XSS: няма SQL, който докосва вход от потребител; миграцията е статичен идемпотентен DDL (CREATE INDEX IF NOT EXISTS); няма dangerouslySetInnerHTML/eval/innerHTML; новите компоненти разчитат на JSX escaping и SSR-safe почистване на listener-ите.
  • Supply chain и CSS: няма промени по package.json/lockfile/CI/конфигурация, а app.css няма външни url(), @import или javascript:/expression() sink-ове.

Две малки, неблокиращи бележки:

  1. Дифът маха директния тест it('keys response-affecting params…') (assert-ите ?bids=1 спрямо без-bids и page=2 спрямо page=5). Guard-ът доказва, че всеки param е в allow-list-а, но вече не твърди, че различни стойности дават различни ключове — а понеже Web cache poisoning: /contracts edge-cache key omits a response-affecting query parameter #56 беше първоначалният инцидент, един ред behavioral assert е евтина застраховка.
  2. 6-те предварително добавени параметъра, overrun индексът и неговият консуматор OVERRUN_WHERE още ги няма на този branch — нормално за база на stack, но моля уверете се, че влизат заедно с route-овете си в feat(web): договори — обзор (лещи време/CPV/кръстосано) #170/feat(web): overruns dashboard #171, а не дрейфват самостоятелно.

Нито една от двете бележки не блокира merge.

Заключение: Одобрявам — няма открити проблеми със сигурността или интегритета на данните; OWASP-съвместимо. Двете бележки по-горе са по желание.

@StanislavBG

Copy link
Copy Markdown
Contributor Author

@lyubomir-bozhinov

Copy link
Copy Markdown
Collaborator

Потвърждавам за merge: двата предишни блокера са затворени (6d3ee42), базата (MetricInfo, fullscreen, design-system) е чиста, @ydimitrof approve-на за сигурност/интегритет. От наша страна — одобрявам. Начало на стека → merge преди #170/#171/#172.

@ydimitrof

Copy link
Copy Markdown
Contributor

Прегледах PR #169 стриктно и локално — за сигурност, интегритет на данните, SQL/injection вектори и злонамерен код. Пуснах тестовете сам и проверих всяко твърдение от дискусията вместо да го приема наготово.

Обхват на промяната
Базата на дизайн-системата за дашбордите: два самостоятелни UI примитива (MetricInfo, FullscreenButton/useFullscreen), дизайн-токени + стилове в app.css, allow-list за cache-key параметрите и partial индекс за overrun агрегатите. PR 1/4 от стек — блокира останалите, затова прегледът е внимателен.

Какво потвърдих локално (fdc149a)

  • CWE-349 / cache poisoning (Web cache poisoning: /contracts edge-cache key omits a response-affecting query parameter #56): drift guard-ът е върнат и работи реално — статично сканира app/** и налага consumed ⊆ CACHE_QUERY_PARAMS ∪ INTENTIONALLY_UNKEYED. Тестовете минават (9/9), включително възстановения поведенчески assert (?bids=1 vs без-bids и page=2 vs page=5 → различни ключове). Махнат е само stale-entries assert-ът — коректно, защото сигурностната посока е обратната. Новите a/b/by/metric/cohort/cpv не се консумират от нито един route на този branch (проверих с grep), значи са безвредно предварително добавяне — в най-лошия случай леко по-нисък cache-hit, никога грешно тяло на отговора.
  • Миграция 0002: статичен идемпотентен DDL (CREATE INDEX IF NOT EXISTS ... WHERE annex_count > 0), нула вход от потребител, нула SQL injection повърхност. migrations.test.ts минава и проверява наличието на индекса. Консуматорът OVERRUN_WHERE още го няма тук — нормално за база на стек; моля влиза заедно с route-овете си в feat(web): договори — обзор (лещи време/CPV/кръстосано) #170/feat(web): overruns dashboard #171, а не самостоятелно.
  • Injection / XSS (OWASP A03): няма dangerouslySetInnerHTML, eval, innerHTML; компонентите разчитат на JSX escaping. MetricInfo е SSR-safe (затворен initial render, listener cleanup, feature-detected), aria-label-ът отразява само подадени string-ове. useFullscreen чисти listener-а и no-op-ва грациозно.
  • CSS sinks: app.css няма външни url(), javascript: или expression(); единственият @import е предсъществуващият tailwindcss. Risk-стиловете (.risk-indicators/.risk-title/.risk-list) са възстановени (7 селектора) — страницата на договора пак е оформена.
  • Supply chain (OWASP A06): нула промени по package.json/lockfile/CI/конфигурация. Нула следи от backdoor, обфускация или code injection.

Заключение по OWASP: не откривам критични уязвимости; входната валидация, cache-ключването и DDL-ът са коректни и съвместими с OWASP Top 10.

Двамата предходни рецензенти вече затвориха двата реални блокера; независимата ми проверка го потвърждава. Останалите бележки са по желание и не блокират merge.

Вердикт: Одобрявам — няма открити проблеми със сигурността или интегритета на данните; OWASP-съвместимо.

@StanislavBG

Copy link
Copy Markdown
Contributor Author

Rebase-нат върху новия main (5d99fd9) с css split: токените → styles/tokens.css, MetricInfo/fullscreen → styles/components.css, останалото → styles/pages.css; app.css е недокоснат (само @imports). Проверено: компилираният CSS е правило-по-правило идентичен с пред-rebase версията (1298=1298), prettier-чист, линейна история, пълен локален browser тест на всички страници без грешки.

@todorkolev — стекът е готов в ред #169 → #170 → #171 → #172 (нови heads: 764a4be / 02e3e42 / 1c120d9 / e884d6c).

@StanislavBG

Copy link
Copy Markdown
Contributor Author

Подобрен MetricInfo: текстът вече никога не прелива (white-space reset + overflow-wrap: anywhere, по-широка карта 320px с viewport clamp) — 32fa5e9. Стекът е ре-каскадиран, нови heads: trends 81f439d · overruns 108d095 · analyze 87546d3.

@ydimitrof

Copy link
Copy Markdown
Contributor

Verification complete. All checks pass on current HEAD (32fa5e9): cache-key drift guard + behavioral assert 9/9, migration test green, typecheck clean, no injection sinks, risk styles restored. The components/index/params are intentionally unused on this stack-base branch.

Прегледах PR #169 стриктно и локално на текущия връх (32fa5e9) — за сигурност, интегритет на данните, SQL/injection вектори, XSS и злонамерен код. Пуснах тестовете сам, сканирах независимо консумираните query-параметри и проверих всяко твърдение от дискусията, вместо да го приемам наготово.

Обхват
База на дизайн-системата за дашбордите (PR 1/4 от стек, блокира останалите): два самостоятелни UI примитива (MetricInfo, FullscreenButton/useFullscreen), split на стиловете в styles/*.css, allow-list за cache-key параметрите и partial индекс за overrun агрегатите.

Какво потвърдих локално

  • CWE-349 / cache poisoning (Web cache poisoning: /contracts edge-cache key omits a response-affecting query parameter #56): drift guard-ът е върнат и работи реално — consumedQueryParams() статично сканира app/** (не само loader-и, а и lib/filters.ts и SSR компонентите) и налага consumed ⊆ CACHE_QUERY_PARAMS ∪ INTENTIONALLY_UNKEYED. Тестовете минават 9/9, включително възстановения поведенчески assert (?bids=1 vs без-bids и page=2 vs page=5 → различни ключове). Махнат е само stale-entries assert-ът — коректно, защото сигурностната посока е обратната. Направих независим grep на всички .get/.getAll/getMulti четения в app/**: единственият параметър извън allow-list-а (range) е фалшив резултат — това е Headers.get('range') в тестов файл + R2 опция, не URL параметър. Тоест нито един реално консумиран param не липсва от ключа. Новите a/b/by/metric/cohort/cpv не се консумират от нито един route тук — безвредно предварително добавяне (най-лошо: леко по-нисък cache-hit, никога грешно тяло).
  • Миграция 0002: статичен идемпотентен DDL (CREATE INDEX IF NOT EXISTS ... WHERE annex_count > 0) — коректен partial индекс, нула вход от потребител, нула SQL injection повърхност. migrations.test.ts минава и потвърждава наличието на индекса.
  • Injection / XSS (OWASP A03): няма dangerouslySetInnerHTML, eval, innerHTML; компонентите разчитат на JSX escaping. MetricInfo е SSR-safe (затворен initial render, listener cleanup за pointer/keydown, aria-label отразява само подадени string-ове); useFullscreen е feature-detected, чисти listener-а и no-op-ва грациозно.
  • CSS sinks: нула url(), @import, javascript: или expression() в новите styles/*.css. Risk-стиловете (.risk-indicators/.risk-title/.risk-list) са възстановени в pages.css и RiskIndicators.tsx пак е оформен.
  • Supply chain (OWASP A06): нула промени по package.json/lockfile/CI/конфигурация. Typecheck е чист. Нула следи от backdoor, обфускация или code injection.

Заключение по OWASP: не откривам критични уязвимости; cache-ключването, входната валидация и DDL-ът са коректни и съвместими с OWASP Top 10.

Неблокиращи бележки (по желание):

  1. Трите примитива, overrun индексът и 6-те нови параметъра нямат консуматор на този branch — нормално за база на стек, но моля уверете се, че влизат заедно с route-овете/заявките си в feat(web): договори — обзор (лещи време/CPV/кръстосано) #170/feat(web): overruns dashboard #171/feat(web): analyze index — five equal analysis cards #172 (OVERRUN_WHERE още го няма тук), а не дрейфват самостоятелно.
  2. Съкратеният хедър коментар в cache-key.ts вече казва само „consumed by apps/web/app/routes loaders“ — подценява реалния обхват на guard-а (цялото app/**, вкл. SSR компоненти като SiteHeader). Коментарът в теста е точен; струва си едноредова корекция за да не заблуди бъдещ разработчик.

Вердикт: Одобрявам — няма открити проблеми със сигурността или интегритета на данните; OWASP-съвместимо. Двете бележки по-горе не блокират merge.

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.
@StanislavBG

Copy link
Copy Markdown
Contributor Author

.lens-* / .analytics-lenses правилата са възстановени байт-идентично (1c678bd) — standalone merge на #169 вече не чупи /analytics; изтриването им се мести в #172, където страницата спира да ги ползва. Нов head: 957ad75; стекът е ре-каскадиран: #170 06a4543 → #171 e28b715 → #172 b864779.

@StanislavBG

Copy link
Copy Markdown
Contributor Author

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

@ydimitrof

Copy link
Copy Markdown
Contributor

Прегледах PR #169 наново и стриктно — локално на текущия връх (957ad75) — за сигурност, интегритет на данните, SQL/injection вектори, XSS и злонамерен код. Инсталирах зависимостите, пуснах тестовете сам и проверих всяко твърдение от дискусията, вместо да го приемам наготово. Този преглед е повторен: при предишен pass бях блокирал PR-а за регресия на /analytics (изтрити .lens-* стилове), затова първо потвърдих, че точно това е затворено.

Обхват
База на дизайн-системата за дашбордите (PR 1/4 от стек — блокира #170/#171/#172): два самостоятелни UI примитива (MetricInfo, FullscreenButton/useFullscreen), split на стиловете в styles/*.css (app.css държи само @import-и), allow-list за cache-key параметрите и partial индекс за overrun агрегатите.

Какво потвърдих локално (957ad75)

  • Регресията на /analytics (моят предишен блокер) — затворена. .lens-* и .tiles.analytics-lenses правилата са възстановени в styles/layout.css (26 селектора), а изтриването им коректно е преместено в feat(web): analyze index — five equal analysis cards #172, където страницата спира да ги ползва. Standalone merge на feat(web): dashboard design-system base (MetricInfo, fullscreen, tokens, overrun index) #169 вече не оставя hub-картите без оформление.
  • Risk стилове — възстановени. .risk-indicators/.risk-title/.risk-list (7 селектора) са в styles/pages.css; страницата на договора пак е оформена.
  • CWE-349 / cache poisoning (Web cache poisoning: /contracts edge-cache key omits a response-affecting query parameter #56): drift guard-ът работи реално — consumedQueryParams() статично сканира целия ../app/**/*.{ts,tsx} през import.meta.glob (не само loader-и) и налага consumed ⊆ CACHE_QUERY_PARAMS ∪ INTENTIONALLY_UNKEYED. Тестовете минават 9/9, включително поведенческите assert-и (?bids=1 vs без-bids и page=2 vs page=5 → различни ключове). Направих независим grep на всички .get/.getAll четения в app/**: единственият параметър извън allow-list-а (range) е Headers.get('range') в тестов файл, не URL параметър — фалшив резултат. Нито един реално консумиран param не липсва от ключа. Новите a/b/by/metric/cohort/cpv не се четат от нито един route тук — безвредно предварително добавяне (най-лошо: леко по-нисък cache-hit, никога грешно тяло).
  • Миграция 0002: статичен идемпотентен DDL (CREATE INDEX IF NOT EXISTS idx_contracts_overrun ON contracts(annex_count) WHERE annex_count > 0) — коректен partial индекс, нула вход от потребител, нула SQL injection повърхност. migrations.test.ts минава.
  • Injection / XSS (OWASP A03): няма dangerouslySetInnerHTML, eval, innerHTML; компонентите разчитат на JSX escaping. MetricInfo е SSR-safe (затворен initial render, cleanup на pointerdown/keydown listener-ите, aria-label отразява само подадени string-ове); useFullscreen е feature-detected, чисти fullscreenchange listener-а и no-op-ва грациозно.
  • CSS sinks: нула url(), javascript: или expression() в новите styles/*.css; единственият @import е предсъществуващият tailwindcss.
  • Supply chain (OWASP A06): нула промени по package.json/lockfile/CI/конфигурация. Нула следи от backdoor, обфускация или code injection.

Съответствие с тикета: двата блокера на @lyubomir-bozhinov и моят предишен lens-блокер са затворени; независимата проверка го потвърждава. Обхватът е атомарен и подходящ за база на стек.

Неблокираща бележка (по желание):

  • Хедър коментарът в cache-key.ts още казва „...consumed by apps/web/app/routes loaders“ — подценява реалния обхват на guard-а (цялото app/**, вкл. SSR компоненти). Коментарът в теста е точен; струва си едноредова корекция, за да не заблуди бъдещ разработчик.

Заключение по OWASP: не откривам критични уязвимости; cache-ключването, входната валидация и DDL-ът са коректни и съвместими с OWASP Top 10.

Вердикт: Одобрявам — няма открити проблеми със сигурността или интегритета на данните; OWASP-съвместимо. Бележката по-горе не блокира merge.

@lyubomir-bozhinov

Copy link
Copy Markdown
Collaborator

Одобрявам. Ре-проверих стриктно на текущия връх 957ad75:

  • MetricInfo (ⓘ) — mobile-first е свършен коректно: useLayoutEffect мери popover-а и го отмества да стои на 8px в viewport-а (audit-нато на 320px за крайните колони), click-toggle (не hover-only), outside-click + Esc с cleanup, aria-label носи пълния текст, картата е overflow: visible за да не реже popover-а. SSR-safe. Кредит за реалния 320px audit.
  • cache-key — няма drift: guard-тестът сканира цялото app/ дърво с camelCase-aware regex и пада CI при consumed-param ∉ allow-list.

Без находки по базата на design-system-а.

B353N added a commit to B353N/sigma that referenced this pull request Jul 8, 2026
De-conflict the migration number: 0002 is claimed by the contracts_overrun_index
family (midt-bg#169/midt-bg#170/midt-bg#171/midt-bg#172), 0003 by midt-bg#188 (contract_health), and 0004 by midt-bg#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.

@ydimitrof ydimitrof left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Преглед на PR: feat(web): dashboard design-system base (MetricInfo, fullscreen, tokens, overrun index)

Какво прави PR-ът

Този PR полага основата на дизайн системата за dashboard-а. Включва два малки React компонента (FullscreenButton.tsx, MetricInfo.tsx) и значителен обем нови стилове: изнасяне на inline стилове в CSS класове в components.css (добро за CSP чистота) и ~2373 реда нови стилове в pages.css за мрежов граф с fullscreen/zoom, choropleth карта, analyze-landing, price-anomaly и overruns дашбордите, базирани на дизайн токени.

Сигурност — CLEAN

И в двете прегледани партиди Phase 0 сканирането е чисто: няма твърдо кодирани тайни/ключове, няма нови или променени URL адреси, няма външни @import/url()/expression(), няма нови зависимости и няма обфускирани шаблони. Няма блокиращи проблеми по сигурността.

Силни страни

  • Последователно използване на дизайн токени (var(--ink), var(--accent), var(--rule)) с разумни fallback стойности.
  • Добро внимание към достъпност: aria-label, focus-visible, touch hit-area ≥44px, спазване на WCAG 1.4.1 (цветът не е единственият носител на информация), клавиатурни пътища и prefers-reduced-motion.
  • Обмислени отзивчиви breakpoint-и и добре документиран код с коментари.

Основни забележки (не-блокиращи)

  1. useLayoutEffect и SSR (MetricInfo.tsx): коментарът твърди „SSR-safe", но useLayoutEffect предизвиква предупреждение при server render дори при ранен return. Препоръка: isomorphic layout effect или обикновен useEffect, тъй като изчислението и без това е клиентски път (при open === true).
  2. Остаряло изместване при [open] (MetricInfo.tsx): хоризонталният clamp се смята веднъж при отваряне; при resize/scroll докато popover-ът е отворен стойността остарява и картата отново може да излезе извън екрана. Обмислете слушатели за resize/scroll докато е отворен.
  3. aria-expanded спрямо hover/focus (MetricInfo.tsx): popover-ът се показва визуално чрез :hover/:focus-within, но aria-expanded се обновява само при click и сочи към постоянно aria-hidden="true" контейнер — семантично несъответствие. Влиянието е малко (текстът е в aria-label), но е добре да се изчисти.
  4. Грешни CSS секционни маркери (components.css): секцията отваря с trends-dashboard, а затваря с end overruns-dashboard; маркерът end metric-info popover реално затваря блока list-search. Козметично, но обърква навигацията.
  5. Дублиран коментар (pages.css, ред 869): коментарът за „Regional choropleth (/map)" е добавен наново, макар идентичен вече да съществува преди .map-wrap svg — премахнете единия.
  6. Непоследователна line-clamp съвместимост (pages.css): .clamp2 включва и стандартния line-clamp, но .az-card-desc, .pa-card и .ov-annex-reason разчитат само на -webkit-line-clamp — добавете и стандартното свойство.
  7. Натрупване на !important (pages.css): в .ov-auth-total, .ov-auth-growth(.is-top) и .ov-scatter-dot:hover — приемливо за таблични override-и, но е сигнал за специфичност. Освен това .ov-board-scale е разделен на два блока, които могат да се обединят по-чисто.

Тестове / покритие

В прегледаните партиди няма тестове за новите компоненти (popover toggle, outside-click/Esc, fullscreen feature-detection). За база на дизайн система е приемливо, но абсолютната порта за покритие (≥90%) не е удовлетворена — отбелязано за общата оценка. Забележка: useFullscreen hook-ът е експортиран, но не се използва от FullscreenButton (компонентът приема active/onToggle отвън) — вероятно свързването е в друга част; ако не, hook-ът остава неизползван.

Вердикт

COMMENT — няма блокиращи или security проблеми. Изброените точки са подобрения по достъпност, коректност и козметика.

Comment thread apps/web/app/components/MetricInfo.tsx Outdated
Comment thread apps/web/app/components/MetricInfo.tsx Outdated
Comment thread apps/web/app/components/MetricInfo.tsx Outdated
Comment thread apps/web/app/styles/components.css Outdated
Comment thread apps/web/app/styles/pages.css Outdated
- MetricInfo: use an isomorphic layout effect (useEffect on the server) to
  drop the "useLayoutEffect does nothing on the server" SSR warning.
- MetricInfo: re-clamp the popover horizontal shift on resize/scroll while
  open (rAF-throttled), not only on initial open, so it can't drift out of
  the viewport.
- MetricInfo: sync aria-expanded with keyboard focus (not just click), since
  the popover also reveals on CSS :focus-within.
- components.css: fix mismatched section markers (open/close names now
  match: trends-dashboard, list-search).
- pages.css: remove duplicated "Regional choropleth" comment.

@ydimitrof ydimitrof left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Преглед на PR — dashboard design-system base (MetricInfo, fullscreen, tokens, overrun index)

ВЕРДИКТ: COMMENT — без блокиращи проблеми; няколко бележки за уточняване преди merge.

Какво прави PR-ът

Този PR полага базата на дизайн-системата за таблото и е част от stacked серия. Обхватът включва:

  • Frontend/презентационни компоненти: FullscreenButton, MetricInfo (popover с clamp-математика), чиста функция clampPopoverShift с unit тестове.
  • Дизайн токени: нов tokens.css (OKLCh цветове, документирано WCAG 1.4.1 съответствие — цветът не е единствен носител на смисъл).
  • Кеширане/маршрути: разширение на CANONICAL_QUERY_PARAMS и нови тестове в cache-key.test.ts.
  • База данни: миграция 0002 — частичен, покриващ индекс за overrun (WHERE annex_count > 0) с гардиращ тест.
  • Зависимости: нова devDependency @testing-library/react (+ транзитивни в pnpm-lock.yaml).

Сигурност — ЧИСТО

И трите партиди преминаха Фаза 0 без блокиране: няма твърдо кодирани тайни, няма продукционни URL промени (само тестови фикстури), няма зловреден код, backdoor, обфускация или injection повърхност. Текстът се рендира като екраниран React children; миграцията е статичен DDL без динамичен вход; новите зависимости са широко разпознати dev-only пакети. Тестът за cache-key дори затваря CWE-349 (#56), предотвратявайки колизия/отравяне на кеша при повтарящи се allow-list параметри.

Силни страни

  • clampPopoverShift е коректно изнесена чиста функция с добро покритие, включително граничния случай ляв+десен clamp.
  • MetricInfo балансира listener-ите (resize/scroll/keydown/pointerdown) с коректен cleanup — няма изтичане; SSR-безопасност чрез useIsoLayoutEffect; rAF-throttle и синхронен shiftRef са добре обмислени.
  • Миграцията гардира предиката и реда на колоните на индекса; forward-only без down-миграция е в съответствие с конвенцията на репото.

Бележки (незадължителни, non-blocking)

  1. FullscreenButton без consumer в тази серия файлове — по CLAUDE.md „NO DEAD CODE / NO PARTIAL IMPLEMENTATION“ това би било проблем, но е обозначен като умишлена база на stacked PR. Моля потвърдете, че consumer-ът идва в следваща партида.
  2. a11y несъгласуваност: aria-expanded върху бутон, чийто popover е aria-hidden="true" и без aria-controls.
  3. Отслабен drift-guard в cache-key.test.ts: тестът „does not retain allow-list entries that nothing reads“ е сведен от expect(stale).toEqual([]) до console.info. Засяга само несигурностната посока (сигурностната остава твърдо асертирана) и е добре обоснован, но потвърдете, че е съзнателно и че over-keying на edge кеша е приемлив.
  4. Дребно за производителност: capture-phase scroll listener може да е passive: true.

Тестове

Тестовете за metric-info-clamp, MetricInfo, cache-key и миграцията са смислени (не тривиални). От предоставения diff не може да се потвърди зелен CI и покритие ≥90% за новия код — препоръчва се да се верифицира преди merge.

Заключение: код с високо качество, чист откъм сигурност. Одобрението зависи от потвърждаване на consumer-а за FullscreenButton, съзнателното отслабване на drift-guard теста, зелен CI/покритие и дребните a11y/производителностни бележки.

Comment thread apps/web/app/components/FullscreenButton.tsx
Comment thread apps/web/app/components/MetricInfo.tsx Outdated
Comment thread apps/web/app/components/MetricInfo.tsx Outdated
Comment thread apps/web/app/lib/query-params.ts
Comment thread apps/web/workers/cache-key.test.ts Outdated
…cInfo (midt-bg#169)

- Remove aria-expanded from the trigger button: the popover is purely visual (aria-hidden), and its text is already conveyed via aria-label, so the two were inconsistent.
- Make the capture-phase window scroll listener passive (never calls preventDefault), keeping capture:true symmetric on add/remove so cleanup still detaches it.
- Update MetricInfo.test.tsx's Escape-dismiss assertion to check the is-dismissed class instead of the removed aria-expanded attribute.

@ydimitrof ydimitrof left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Преглед на PR — обобщение

Заглавие: feat(web): dashboard design-system base (MetricInfo, fullscreen, tokens, overrun index)

ВЕРДИКТ: COMMENT — няма блокиращи проблеми по сигурност или коректност; препоръчват се няколко уточнения преди merge.


Какво прави PR-ът

Въвежда базовия слой на dashboard design-system, прегледан на 3 партиди:

  • Партида 1 — React/TS компоненти: MetricInfo (popover с clamp математика, достъпност, SSR-безопасен), FullscreenButton, metric-info-clamp.ts и разширяване на CANONICAL_QUERY_PARAMS в query-params.ts.
  • Партида 2 — презентационни стилове в pages.css (+2372/−3): .net-*, .map-*, .az-*, .pa-*, .ov-* и native fullscreen, с внимание към достъпност (prefers-reduced-motion, WCAG бележки).
  • Партида 3 — токени (tokens.css), частичен overrun индекс (SQL миграция 0002), тестове за миграции и cache-key, нова devDependency @testing-library/react.

Сигурност (Phase 0) — ЧИСТО ✅

Във всичките три партиди: няма твърдо кодирани тайни, няма нови/променени URL адреси, няма инжекции (XSS/SQL), обфускация или бекдори. Всички JSX стойности се escape-ват от React; CSS файлът не изпълнява код; SQL миграцията е идемпотентна CREATE INDEX IF NOT EXISTS без injection повърхност. Единствената нова зависимост (@testing-library/react ^16.3.2, devDependency) е с очаквани транзитивни пакети и съвпадаща резолвната версия. pnpm-lock.yaml е консистентен.

Коректност и качество

  • metric-info-clamp.ts — математиката е вярна и покрита с тестове; съответства на CSS max-width: min(320px, calc(100vw - 16px)).
  • MetricInfo.tsx — внимателно управление на състояния, симетрични listener-и, rAF коалесциране, коректна достъпност (popover aria-hidden, пълен текст в aria-label).
  • Миграция 0002 — добре обоснован частичен индекс (WHERE annex_count > 0, колонен ред (signing_value_eur, current_value_eur)); forward-only без rollback е в съответствие с конвенцията на репото.
  • migrations.test.ts — отличен тест, който пази самата дефиниция на индекса срещу тиха регресия.
  • tokens.css / pages.css — чисти токен-базирани добавки с изрична WCAG 1.4.1 грижа (цветът не е единственият носител на смисъл).

Незадължителни бележки (не блокиращи)

  1. CSS дублиране — .fs-btn vs .trend-fs-btn (партида 1) и двойно дефиниран .ov-board-scale (партида 2); обмислете обща база/сливане.
  2. FullscreenButton без call site — потвърдете, че свързването идва в следваща партида, за да не остане мъртъв код.
  3. query-params.ts — новите канонични параметри са безопасни срещу cache poisoning; единствен ефект е потенциална over-keying фрагментация. Уверете се, че drift guard-ът (cache-key.test.ts) действително покрива новите записи.
  4. Козметика в стиловете — премахнат outline в .network-svg a:focus-visible (компенсиран със stroke пръстен — потвърдете контраста), смесени цветови пространства в color-mix (oklch vs srgb), нарастващ брой !important.
  5. cache-key тест — бившата твърда проверка е превърната в „info“ тест, който винаги минава — гранично „cheater test“.

За потвърждение извън прегледа

  • Реалното минаване на тестовете и покритието (не могат да се изпълнят тук).
  • Твърдението „fully covering“ за overrun индекса зависи от OVERRUN_WHERE в queries/overruns.ts (друга партида).
  • Изричен тест за приложения translate/shift стил при видим popover би бил полезно допълнение.

Заключение: Чист, добре документиран design-system слой без проблеми по сигурност или данни. Забележките са за поддръжка и потвърждения — не блокират merge, но си струва да се адресират.

Comment thread apps/web/app/components/FullscreenButton.tsx
Comment thread apps/web/app/components/metric-info-clamp.ts Outdated
Comment thread apps/web/app/lib/query-params.ts
Comment thread packages/db/migrations/0002_contracts_overrun_index.sql Outdated
Comment thread apps/web/workers/cache-key.test.ts 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.

Ре-верифицирах на HEAD (ffd9d844): новият комит е a11y полиране (махнат stray aria-expanded + passive scroll listener в MetricInfo) — презентационно, не мени cache-key нито данните. Предишният ми преглед (cache-key/CWE-349 чист) стои. Одобрявам.

…ricInfo

TypeScript's EventListenerOptions type for removeEventListener only
accepts capture, not passive (passive is addEventListener-only) —
was breaking CI typecheck.
…mp constant

- cache-key.test.ts: the info-only "stale allow-list entries" check was
  wrapped in an `it()` with no assertion, so it could never fail. Runs as
  plain code in the describe body instead — same console.info signal, not
  registered as a graded test.
- 0002_contracts_overrun_index.sql: the "fully covering" claim only holds for
  the single-pass corpus aggregate; the leaderboard and by-authority/by-sector
  breakdowns JOIN on c.tender_id and read further contracts columns, so they
  still take one table lookup per matching row. Comment corrected.
- metric-info-clamp.ts: named the 8px inset as VIEWPORT_INSET_PX so it can't
  drift from the CSS's `calc(100vw - 16px)` (2x the inset).
The CANONICAL_QUERY_PARAMS drift guard's stale-entry check ran as plain
describe-body code with no expect(), so it could never fail regardless of
whether the guarded behavior broke (CLAUDE.md NO CHEATER TESTS). Re-register
it as a real it() that asserts no unexpected stale entries, excluding the
documented stacked-PR-ahead-of-route params (a, b, by, cohort, cpv, metric)
so legitimate stacked work still doesn't block unrelated PRs.
StanislavBG added a commit to StanislavBG/sigma-pr that referenced this pull request Jul 27, 2026
…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) and valibot to ^1.4.2
(GHSA-5qjj-4xww-7phc) via pnpm overrides - both patch-level, non-breaking
fixes. Add a time-boxed osv-scanner.toml suppression for react-router's
GHSA-qwww-vcr4-c8h2, a CSRF flaw scoped to unstable RSC APIs this app does
not use (verified via repo-wide grep); no fix exists in the 7.x line and
bumping to 8.x is out of scope for this patch.
- osv-scanner.toml: keep both independently-added suppressions (sharp
  GHSA-f88m-g3jw-g9cj from main, react-router GHSA-qwww-vcr4-c8h2 from
  this branch)
- packages/db/migrations: both branches added an unrelated migration
  numbered 0002 (main's 0002_current_value_currency.sql, this branch's
  0002_contracts_overrun_index.sql); renumbered the latter to
  0003_contracts_overrun_index.sql since they are independent (no
  shared table/column) and updated migrations.test.ts to apply both
- pnpm-lock.yaml: regenerated via pnpm install rather than hand-merging
todorkolev added a commit that referenced this pull request Jul 29, 2026
… + 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>
@lyubomir-bozhinov

Copy link
Copy Markdown
Collaborator

Ъпдейт на миграционната номерация (замества по-ранната бележка за 0002 — тя вече е неактуална):

Стекът носи packages/db/migrations/0003_contracts_overrun_index.sql, но main междувременно се напълни до 0005:

main: 0000_init · 0001_flow_pairs_bidder_index · 0002_current_value_currency
      · 0003_related_persons_foundation · 0004_cpv_division_stats · 0005_list_sort_indexes

Значи номер 0003 вече е зает от 0003_related_persons_foundation на main → два различни файла с един и същ номер 0003. Преномерирай overrun index-а на 0006 (следващия свободен) преди merge. #170/#171/#172 носят същия файл, така че се оправя веднъж тук в базата и стекът го наследява; ъпдейтни и препратката в packages/db/src/migrations.test.ts (коментарът още сочи „0002").

(Странична бележка: euro-annex капанът вече е затворен на main чрез 0002_current_value_currency — /overruns, който чете current_value_eur, е коректен на текущия HEAD. Старата загриженост за него отпада.)

@nedda76

nedda76 commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Този клон е в конфликт с main, тъй че към момента не може да се ревюира — дифът, който GitHub показва, вече не отговаря на това, което би влязло. Ще го пребазираш ли върху актуалния main (или merge на main в клона) и да разрешиш конфликтите? След това веднага го поглеждам. Благодаря! 🙏

@ydimitrof ydimitrof left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Преглед на PR — обобщение

Заглавие: feat(web): dashboard design-system base (MetricInfo, fullscreen, tokens, overrun index)

Какво прави PR-ът

Полага базата на дизайн-системата за таблото. Обхваща три групи промени:

  • UI компоненти: MetricInfo (с изнесена, тествана clamp логика и добър фокус-мениджмънт), презентационен FullscreenButton, канонични query параметри.
  • Стилове: нови дизайн токени (tokens.css) и разширения в pages.css за fullscreen, zoom контроли, overruns и др.
  • Инфраструктура и сигурност: нов частичен БД индекс (миграция 0003 за overruns) с тест, security override-и (sharp, postcss, valibot), OSV ignore за react-router CVE, нова dev зависимост (@testing-library/react) и lockfile.

Обща оценка

Много добре структуриран, щателно коментиран и обоснован код. Достъпността е обмислена последователно (пълни aria атрибути, фокус-мениджмънт, prefers-reduced-motion, :focus-visible, WCAG 1.4.1 — цветът никога не е единственият носител на значение). Логиката е изнесена в чисти, тествани функции; effect-ите почистват listener-ите си. Промените в зависимостите са в правилна посока и адресират реални CVE (само dev/build).

Сигурност (Phase 0): ЧИСТО

  • Няма твърдо кодирани тайни, зловредни шаблони или неоторизирани URL адреси.
  • Промените в зависимости са обосновани: sharp ^0.35.0 (GHSA-f88m-g3jw-g9cj, High), postcss ^8.5.18 (path traversal), valibot ^1.4.2, плюс доверената dev зависимост @testing-library/react.
  • Флаг (не блокер): OSV ignore за react-router (GHSA-qwww-vcr4-c8h2) — обосновката (CSRF само в неизползвани unstable RSC пътища; няма fix в 7.x; 8.x е breaking) е разумна и има краен срок 2026-10-01. Препоръка: потвърдете с ясен grep, че RSC API-тата наистина не се използват никъде.

Основни находки (не блокиращи)

  1. Тестова покривка на toggle поведението (MetricInfo.test.tsx / MetricInfo.tsx:142) — тестът проверява само класа is-open чрез fireEvent.click, който не генерира focus събития. В реален браузър :focus-within държи popover-а видим, така че видимото поведение остава непокрито.
  2. Потенциално неизползван токен --trend-count-ink — самият коментар признава, че още не се консумира; ако не се използва в останалите части, е dead code спрямо CLAUDE.md.
  3. Неточен коментар в migrations.test.ts — казва „0002", а индексът идва от миграция 0003.
  4. EXPECTED_STALE_PLANNED_PARAMS изключва записи (вкл. едносимволните a/b) от stale проверката — риск да маскира истински typo и да отслаби drift guard-а.
  5. CSS почиствания в pages.css: дублиран селектор .ov-board-scale (обединете двете правила); струпани !important (добавете кратък коментар защо са нужни или увеличете специфичността); несъгласуван line-clamp fallback — .az-card-desc и .ov-annex-reason използват само -webkit-line-clamp, добавете и стандартното line-clamp.

Заключение

Няма блокиращи проблеми в нито една партида. Verdict: COMMENT. Кодът е готов за merge след адресиране (по избор) на горните дребни бележки; препоръчително е потвърждаването на react-router RSC допускането и коригирането на неточния тестов коментар преди merge.

Comment thread apps/web/app/components/MetricInfo.tsx Outdated
Comment thread apps/web/app/components/MetricInfo.test.tsx
Comment thread apps/web/app/lib/query-params.ts
Comment thread packages/db/src/migrations.test.ts Outdated
Comment thread apps/web/workers/cache-key.test.ts
# Conflicts:
#	osv-scanner.toml
#	packages/db/src/migrations.test.ts
#	pnpm-lock.yaml
#	pnpm-workspace.yaml
MetricInfo's close click gave the button focus (Chrome/Firefox), which kept
`.metric-info:focus-within .metric-info-pop` active after `is-open` was
removed — the popover stayed visible until focus left some other way. Blur
the button on the open->closed transition so CSS visibility tracks `open`
immediately. Extend the test to focus the button post-click and assert real
focus/visibility state, not just the `is-open` class — it fails against the
pre-fix component.

Also pin each stacked-PR-ahead allow-list param (a, b, by, cohort, cpv,
metric) to its destined route via PLANNED_PARAM_ROUTES, and strengthen the
cache-key.test.ts stale-entry guard to assert the raw unconsumed set equals
exactly those six pinned params and that each has a route mapping — so a
typo among the six themselves can no longer be silently masked.
migrations.test.ts had unformatted multiline chain drift already present on
this branch (unrelated to this PR's content); cache-key.test.ts needed
--write for the new PLANNED_PARAM_ROUTES import formatting. Both fixed via
prettier --write so the check job's lint gate is green.

@ydimitrof ydimitrof left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Обобщение на прегледа

Какво прави този PR

Полага основата на дизайн-системата за dashboard-а в apps/web. Промените включват: нови презентационни компоненти (FullscreenButton, MetricInfo с popover логика и изнесената, тествана функция clampPopoverShift), разширяване на query-params.ts (CANONICAL_QUERY_PARAMS и PLANNED_PARAM_ROUTES), нови статични стилове за dashboard (components.css, pages.css — net-graph, map, analyze-landing, price-anomaly, overruns), дизайн-токени в tokens.css по съществуващата OKLch схема, forward-only миграция 0011 с частичен индекс, добавяне на dev-зависимостта @testing-library/react (с точно съответстващи промени в pnpm-lock.yaml) и придружаващи тестове.

Обща оценка

Чиста, добре обмислена и много добре покрита с тестове промяна и в трите партиди. Няма злонамерен или подозрителен код, неочаквани мрежови извиквания, ексфилтрация, промени в auth/CI/permissions или supply-chain рискове. Всички нови transitive зависимости са легитимни.

Силни страни

  • MetricInfo коректно почиства resize/scroll слушателите, използва rAF за коалесиране на scroll и третира shiftRef като синхронно огледало, за да е преизчисляването идемпотентно; достъпността е добра (пълен текст в aria-label, aria-hidden popover, Esc и outside-click за touch пътя).
  • clampPopoverShift е чиста, изнесена и тествана; PLANNED_PARAM_ROUTES прави stacked-PR изключенията проверими от drift guard-а.
  • CSS следва конвенциите, използва app tokens последователно с разумни fallback-и, а достъпността е взета предвид (prefers-reduced-motion, focus-visible ринг, легенди към цветовите маркери за WCAG 1.4.1).
  • Токените са добре документирани и внимателно отбелязват, че цветът никога не е единственият носител на смисъл. Миграцията е forward-only по конвенцията, а частичният индекс е разумно проектиран и обоснован. Тестовете защитават реални инварианти (CWE-349 cache collapse, дефиниция на индекса, инвентар на миграциите).

Блокиращи концерни

Няма блокери.

Незадължителни бележки (за потвърждение преди merge)

  • Теоретична гонка в MetricInfo-ефекта: ако scroll се задейства между setShift(next) и commit-а на React, getBoundingClientRect() ще отрази стария translate, докато shiftRef вече е новата стойност. На практика незначимо (rAF коалесиране + синхронен commit на layout ефектите) — не води до дефект.
  • Един въпрос за уточнение относно реда на прилагане в теста за миграциите (виж inline коментара) — не е блокер, но си струва да се потвърди преди merge.

Одобрено, при условие че уточнението по теста за миграциите бъде потвърдено.

Comment thread apps/web/app/styles/pages.css
Comment thread packages/db/src/migrations.test.ts
…n test

Reorder .clamp2 declarations to match .az-card-desc/.ov-annex-reason, and
apply the full 0000-0011 migration chain in the served-schema test instead
of skipping 0004-0010, so a future column rename in that range fails loudly.

@ydimitrof ydimitrof left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Обобщение на прегледа

Какво прави този PR

Въвежда базата на дизайн-системата за дашборда: нови компоненти MetricInfo и FullscreenButton, помощни модули (metric-info-clamp.ts, query-params.ts), разширения в components.css и pages.css (мрежов граф, карта, analyze-landing, price-anomaly и overruns табла, презентационни токени), нова миграция 0011_contracts_overrun_index.sql за частичен overrun индекс, както и съпътстващи тестове и зависимостта @testing-library/react. Прегледът беше извършен на 3 партиди.

Обща оценка

Чиста, добре обмислена и добре покрита с тестове промяна. Няма злонамерен или обфускиран код, няма нови мрежови повиквания, промени по auth/CI или изтичане на тайни, няма опити за prompt injection в коментарите. И трите партиди са одобрени.

Най-важни находки (положителни)

  • Кеш ключове / сигурност: разширяването на CANONICAL_QUERY_PARAMS е предварително за бъдещи маршрути и е обезопасено от PLANNED_PARAM_ROUTES + drift guard. Новите проверки в cache-key.test.ts укрепват защитата срещу колизии на кеш ключове (CWE-349).
  • Коректност (MetricInfo): правилно управление на фокуса, Esc-dismiss и нулиране на dismissed; layout ефектът чете shiftRef.current, избягвайки остарели closures; идемпотентността е покрита с тест. Няма течове на ресурси — resize/scroll слушателите и rAF се почистват коректно (потвърдено с тест).
  • Математика (metric-info-clamp.ts): чиста и тествана; приоритетът ляв/десен clamp съответства на CSS max-width.
  • Миграция 0011: коректна и идемпотентна (IF NOT EXISTS), частичният индекс е подмножество на OVERRUN предиката, forward-only по конвенция; тестът проверява точната дефиниция (предикат + ред на колоните).
  • Зависимости: @testing-library/react и транзитивните ѝ записи в pnpm-lock.yaml са легитимни и съответстват на декларираната версия.
  • CSS/токени: следват конвенциите (var(--...), без нови inline style=); коментарите за WCAG 1.4.1 са уместни.

Блокиращи опасения

Няма. Одобрено и от трите партиди.

Дребни, необвързващи забележки

  • FullscreenButton все още няма консуматор, а токенът --trend-count-ink и части като .fs-btn/.trend-fs-btn се дублират/не се ползват — умишлена основа за следващи партиди на този stacked-PR.
  • Две дребни бележки по поддръжка в pages.css.
  • Мъртъв код в един от тестовете (отбелязан инлайн).

Comment thread apps/web/app/styles/pages.css
Comment thread apps/web/workers/cache-key.test.ts Outdated
StanislavBG and others added 3 commits September 19, 2026 22:57
Renumber contracts_overrun_index 0011 -> 0023 (main owns 0011-0022), take main's
pnpm catalog/lockfile (regenerated) and coverage baseline.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…stale assert в cache-key.test

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
StanislavBG added a commit to StanislavBG/sigma-pr that referenced this pull request Sep 20, 2026
… номериране на миграциите, coverage baseline, споделени резолюции

This branch has not been deployed

No deployments
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