Skip to content

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

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

feat(web): dashboard design-system base (MetricInfo, fullscreen, tokens, overrun index)#169
StanislavBG wants to merge 20 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).

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

Copy link
Copy Markdown
Contributor Author

Addressed all 5 threads in 7c7e56a:

  • SSR `useLayoutEffect` warning → isomorphic layout effect (`useEffect` on the server).
  • Popover horizontal clamp now re-clamps on `resize`/`scroll` while open (rAF-throttled), not only on initial open.
  • `aria-expanded` now syncs on focus/blur, matching the CSS `:focus-within` reveal.
  • `components.css`: fixed mismatched section close markers (`trends-dashboard`, `list-search`).
  • `pages.css`: removed the duplicated "Regional choropleth" comment.

typecheck/test/format all green.

StanislavBG added a commit to StanislavBG/sigma-pr that referenced this pull request Jul 10, 2026
- MetricInfo: isomorphic layout-effect guard to drop SSR warning (as midt-bg#169)
- overruns: bidderEik falls back on empty string too, not just null/undefined
- trends: guard CPV cohort relLabel against a zero median (no ×Infinity)
- overruns: fix SECTION comment numbering + mast lede to match actual render order
- components.css: unify open/close dashboard section markers, dedupe .fs-btn/.trend-fs-btn, add missing :focus-visible
- tokens.css: --paper-raised as oklch(100% 0 0) instead of raw hex
- cache-key.test.ts: soft guard requiring a route/pending note on unconsumed allow-list keys
- ComboTrendChart: document the partialIdx > 0 assumption

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

Обобщение на прегледа — feat(web): dashboard design-system base

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

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

Полага базата на дизайн-системата за дашборда: нов компонент MetricInfo (popover с подсказка) и FullscreenButton, ~1000 реда стилове в components.css, +2372 реда в pages.css, нови токени в tokens.css (slate/tan/ochre + paper-raised, Inter премахнат), разширяване на allow-list-а за кеш ключове (cache-key.ts) за предстоящите маршрути (/compare, /overruns, /price-anomaly) и нов частичен индекс за overrun агрегати (0002_contracts_overrun_index.sql) с тестове.

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

Всички три партиди преминават Фаза 0 без забележки: няма твърдо кодирани тайни, няма нови/променени URL адреси, зависимости, dangerouslySetInnerHTML, eval, инжекция или обфускация. Целият текст минава през React (авто-escape). SQL миграцията е статичен DDL; тестовете подават фиксиран SQL без интерполация. Промяната по cache-key.ts дори подобрява защитата срещу CWE-349 (грешно споделяне на кеш) — критичният drift-guard е запазен и се налага в CI, така че свръх-кеиране е максималната цена, никога грешно тяло.

Силни страни

  • SSR-безопасност (useIsoLayoutEffect, feature-detection), коректно почистване на всички event listener-и, rAF коалесциране на scroll/resize.
  • Отлична достъпност: пълен aria-label, :focus-within, touch fallback, outside-click/Esc затваряне, ≥44px touch target; prefers-reduced-motion; цветът никога не е единствен носител на смисъл (WCAG 1.4.1).
  • Миграцията е адитивна, идемпотентна (IF NOT EXISTS), с реален тест за наличие на индекса; намалява пълните сканирания.

Неблокиращи бележки

  1. Два механизма за цял екран — native Fullscreen API hook (useFullscreen) срещу CSS модал (.trend-chart-panel--full). Потвърдете кой се използва, за да няма мъртъв код.
  2. Празен .catch(() => {}) в useFullscreen.toggle поглъща грешки без лог.
  3. Esc не затваря popover, отворен само чрез фокус (ARIA tooltip шаблон изисква скриване).
  4. cursor: help върху интерактивен toggle бутон — pointer е по-подходящо.
  5. Потвърдете color-mix(in oklch) / самостоятелното translate спрямо матрицата от поддържани браузъри.
  6. Дублиран селектор .ov-board-scale и непоследователен line-clamp (липсва стандартното свойство до -webkit-line-clamp в .az-card-desc, .ov-annex-reason).
  7. !important в няколко .ov-* селектора — потвърдете, че е за data-driven стойности, а не специфичностен конфликт.
  8. Козметично: коментарът end trends-dashboard затваря блок, отворен като price-anomaly.

Гейт за тестове

Има солидно покритие за миграцията и drift-guard-а, но липсват unit тестове за MetricInfo/useFullscreen (clamp/shift, toggle, outside-click, Esc). Ако не съществуват в друга партида, гейтът за покритие ≥90% не е изпълнен.

Кросбач потвърждение преди merge

Пълнотата на allow-list-а за кеш ключове зависи от това маршрутите (в другите партиди) да четат query параметрите с литерални ключове (sp.get('cpv')), а не динамично — иначе минават покрай статичния скенер. Моля потвърдете това при ревюто на партидите с маршрутите.

Обща оценка: нисък риск, качествена и фокусирана промяна без scope creep. Готово за merge след изясняване на въпроса за тестовете и кросбач потвърждението по-горе.

Comment thread apps/web/app/components/FullscreenButton.tsx Outdated
Comment thread apps/web/app/components/FullscreenButton.tsx Outdated
Comment thread apps/web/app/components/MetricInfo.tsx
Comment thread apps/web/app/components/MetricInfo.tsx
Comment thread apps/web/workers/cache-key.ts Outdated
Comment thread apps/web/workers/cache-key.test.ts Outdated
Comment thread packages/db/migrations/0002_contracts_overrun_index.sql Outdated
Comment thread apps/web/app/styles/tokens.css Outdated
…n index

- log FullscreenButton's requestFullscreen failure instead of swallowing it
- MetricInfo: Esc now dismisses a hover- or focus-only-opened popover (not
  just click-opened), aria-expanded reflects hover too, and the popover's
  shift-clamp math moves to a testable pure function with unit coverage at a
  320px viewport
- restore a soft, non-failing cache-key allow-list drift check (info-only)
- widen idx_contracts_overrun to a composite (signing_value_eur,
  current_value_eur) partial index — EXPLAIN QUERY PLAN against a ~190k-row
  fixture showed it fully covers OVERRUN_WHERE's aggregates, ~4x faster than
  the annex_count-only index
- convert --paper-raised to oklch(100% 0 0) to match the file's token
  convention (identical rendering to #ffffff)

Confirmed no dynamic (non-literal) query-param reads in the cache-key
allow-listed routes (trends/competition/map/flows) — all sel() call sites
pass literal keys, so no code change was needed for that thread.
StanislavBG added a commit to StanislavBG/sigma-pr that referenced this pull request Jul 11, 2026
- trends.tsx: remove dead singleSelectFilters import
- cache-key.test.ts: restore soft, non-failing allow-list drift check
  (reusing the pattern already landed on pr/dash-base midt-bg#169, not a
  second copy)
- trend.ts: document getCpvGroupStats' full-scan perf tradeoff with a
  TODO for future precompute if corpus size grows

ComboTrendChart's tooltip role="status" stays as-is: its per-hover
data isn't exposed accessibly anywhere else on the page, so removing
the live region would silently drop that data for screen readers.

filters.ts:41 (cpvGroupSelection validation) verified correct as-is
per reviewer confirmation — no code change.

@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 база (MetricInfo, fullscreen, tokens, overrun index)

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

Въвежда базата на design-system за dashboard-а: презентационни React/TSX компоненти (MetricInfo, FullscreenButton), чиста помощна функция за клампване на popover (metric-info-clamp.ts) с тестове, и значителен набор от стилове (components.css и +2372 реда в pages.css, покриващи .net-*, .map-*, .az-*, .pa-*, .ov-* overrun index). Промените са изцяло във фронтенд презентационния слой (React + CSS), без бекенд логика.

Сигурност — CLEAN

И двете партиди преминаха задължителния security скрийнинг чисто: няма твърдо кодирани тайни, няма нови/променени URL адреси или външни заявки, няма нови зависимости, няма инжекционна повърхност (без dangerouslySetInnerHTML, eval, expression(), url(javascript:) или външни @import). Не се изисква human security approval.

Блокиращи констатации

  1. [Коректност] Клампът на popover-а не е идемпотентен спрямо resize/scroll. В MetricInfo.tsx recompute() чете getBoundingClientRect() след като translate: ${shift}px вече е приложен, затова при следващ resize/scroll изчислява от отместения rect и на практика нулира клампа — popover-ът пак излиза извън екрана (напр. естествено [96,328]@vw=320 → shift=-16 → rect [80,312] → resize → dx=0 → отново клипва). Това обезсмисля логиката, за която са писани тестовете. Клампвай спрямо неотместена база.
  2. [Покритие] Липсват тестове за интерактивните компоненти. Покрита е само чистата clamp функция; MetricInfo (Esc / outside-click / state машина) и useFullscreen/FullscreenButton нямат тестове. Гейтът иска ≥90% покритие за нов код, а най-регресивно-податливата логика остава непокрита.

Не-блокиращи (дребни)

  • Клампът се смята само при клик-open, не при hover/:focus-within разкриване — при align='end' на тесен екран картата може да клипне.
  • Заблуждаващ тест: „prioritizes the left clamp when both edges clip“ реално задейства само лявото условие (right=304 < 312); добави кейс, в който клипват и двата ръба.
  • CSS: дублирани правила за един селектор (.ov-board-scale, .ov-panel-note) — струва си консолидация; непоследователен line-clamp (липсва стандартният до -webkit-line-clamp); локализирани !important (specificity смущение); зависимост от модерни фийчъри (:has(), color-mix, :fullscreen) без graceful fallback — да се потвърди спрямо матрицата за поддръжка.

Положителни неща

SSR-безопасни ефекти (useLayoutEffectuseEffect, четене на document само в ефекти), изрядно почистване на всички listener-и (без изтичане на ресурси), feature-detection на requestFullscreen/exitFullscreen, солидна достъпност (aria-pressed/aria-expanded/aria-label, aria-hidden на декоративен SVG, ≥44px touch target, :focus-visible рингове, цветът не е единствен носител на информация, prefers-reduced-motion). CSS е изнесен от inline style= заради CSP.

Отворени въпроси (за проверка в останалите партиди)

  • useFullscreen изнася isFullscreen/toggle, но FullscreenButton приема active/onToggle — да се потвърди, че hook-ът реално се ползва (иначе потенциален мъртъв код).
  • Фокус-trap/възстановяване на фокуса за fullscreen модала (.trend-chart-panel--full) вероятно е в routes/trends.tsx.

ВЕРДИКТ: Заявка за промени (REQUEST_CHANGES) — заради самонулиращия се кламп (№1) и липсващото тестово покритие за интерактивните компоненти (№2). Няма проблеми със сигурността; останалите находки са дребни/козметични.

Comment thread apps/web/app/components/MetricInfo.tsx
Comment thread apps/web/app/components/MetricInfo.tsx Outdated
Comment thread apps/web/app/components/metric-info-clamp.test.ts Outdated
…-bg#169 review)

- MetricInfo: recompute the horizontal clamp from the popover's unshifted
  natural rect (getBoundingClientRect includes the currently-applied
  translate), so repeated resize/scroll recomputes converge instead of
  compounding or cancelling the previous shift.
- MetricInfo: the clamp effect now re-runs on any reveal path (click, hover,
  keyboard focus), not just the click-toggled `open` state, so a narrow-screen
  hover/focus reveal gets clamped too.
- metric-info-clamp.test.ts: fix the "both edges clip" fixture so the right
  clamp genuinely fires before the left clamp overrides it, and assert both
  clamp conditions are true.

@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 база (MetricInfo, fullscreen, tokens, overrun индекс)

Обобщен вердикт: REQUEST_CHANGES — блокира само по един gate (тестово покритие); няма проблеми със сигурността.

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

Въвежда базата на дизайн-системата за dashboard-а: компонент MetricInfo с popover и clamp-логика, fullscreen функционалност (FullscreenButton + useFullscreen), CSS токени (OKLch цветове, --paper-raised) и стилове за страници/компоненти, разширяване на cache-key параметрите в worker-а и нов частичен покриващ индекс за overrun заявки (0002_contracts_overrun_index.sql). Прегледът е направен на 3 партиди.

Сигурност — ЧИСТО (и трите партиди)

Няма твърдо кодирани тайни, нови/променени URL адреси, нови зависимости или зловредни шаблони (eval, innerHTML, dangerouslySetInnerHTML, expression(), javascript:). Текстът се рендира като auto-escape JSX; SQL миграцията е статичен DDL без интерполиран вход — няма вектор за инжекция.

Силни страни

  • clampPopoverShift е коректно отделена чиста функция с добро тестово покритие (математиката е проверена ръчно).
  • SSR-безопасност навсякъде, изрядно почистване на listeners/rAF (без течове), shiftRef избягва stale closures.
  • Добра достъпност: aria-label, Esc-затваряне, ≥44px touch target, WCAG 1.4.1 (цветът не е единствен носител на смисъл), prefers-reduced-motion, видими focus рингове.
  • Cache-key промените само добавят ключувани параметри — безопасно (по-грануларен ключ никога не връща грешно тяло); drift guard-ът остава твърд.
  • Частичният индекс е добре проектиран (предикатът съвпада със заявката, IF NOT EXISTS → идемпотентност).

Блокиращ проблем

  1. Тестово покритие под gate-а (≥90%). Тестван е само чистият clampPopoverShift. Стейт-машината на MetricInfo (open/hover/focus/dismissed, Esc, outside-click, recompute) и целият useFullscreen hook са без тестове — точно най-рисковият за регресии код. Нужни са компонентни тестове (напр. @testing-library/react).

Незадължителни бележки

  1. Един въпрос за потвърждение (cache-key): обработва ли cacheKey() повтарящия се cohort по всички стойности? Ако пази само първата, ?cohort=a&cohort=b и ?cohort=a може да се сблъскат в кеша (CWE-349).
  2. Кросбраузър: fullscreen (useFullscreen и :fullscreen CSS) използва само стандартния API без webkit-префикс — тихо no-op на по-стари Safari. Да се потвърди дали е извън обхвата.
  3. CSS токен/коментар: --trend-count-ink: #5e7c8b е коментиран „darker slate", но е идентичен на --trend-count — да се коригира стойността или коментара.
  4. CSS дублиране: .fs-btn/.trend-fs-btn и дублираният .ov-board-scale — по-чисто с общ базов клас/сливане (функционално коректни).
  5. Дребно CSS: липсва стандартен line-clamp до -webkit-line-clamp на места; няколко !important подсказват проблем със специфичността.
  6. A11y дребно: aria-expanded се сменя, но popover-ът е aria-hidden без aria-controls — няма реален разгънат регион (не блокира предвид дизайна).
  7. Обратимост: обмислете down-миграция/бележка за индекса и проверка на частичния предикат (не само името) в теста.
  8. Да се потвърди, че useFullscreen се консумира някъде (иначе потенциален dead code).

Заключение: PR-ът е чист откъм сигурност и добре документиран. Единственият блокер е тестовото покритие на интерактивната логика (т.1); останалите бележки са козметични/незадължителни.

Comment thread apps/web/app/components/MetricInfo.tsx
Comment thread apps/web/app/components/FullscreenButton.tsx Outdated
Comment thread apps/web/app/styles/components.css
Comment thread apps/web/workers/cache-key.ts Outdated
Comment thread apps/web/workers/cache-key.test.ts Outdated
Comment thread packages/db/migrations/0003_contracts_overrun_index.sql
Comment thread packages/db/src/migrations.test.ts
- add MetricInfo component tests (click toggle, Esc-on-hover close,
  symmetric resize/scroll listener cleanup) via @testing-library/react
- remove the dead useFullscreen hook (zero consumers); keep the
  presentational FullscreenButton for a future caller to wire up
- correct the misleading --trend-count-ink comment (byte-identical to
  --trend-count, unconsumed) instead of inventing a new color
- lock in repeated-cohort cache-key behavior with a regression test
- document why the stale-allow-list-entry check is intentionally
  unbounded console.info, not a hard failure
- assert the idx_contracts_overrun index's actual predicate + column
  order, not just its presence
- document 0002's forward-only migration convention instead of adding
  a DROP INDEX
# Conflicts:
#	apps/web/app/styles/components.css
#	apps/web/workers/cache-key.test.ts
#	apps/web/workers/cache-key.ts
#	pnpm-lock.yaml

@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
…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 в клона) и да разрешиш конфликтите? След това веднага го поглеждам. Благодаря! 🙏

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