feat(web): dashboard design-system base (MetricInfo, fullscreen, tokens, overrun index) - #169
StanislavBG wants to merge 28 commits into
Conversation
lyubomir-bozhinov
left a comment
There was a problem hiding this comment.
Благодаря — основата е добре структурирана: 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.
|
Благодаря за внимателния преглед — и двата блокера са затворени (force-push 🔴 CWE-349 drift guard възстановен. Върнах 🟠 Risk стиловете върнати. |
|
Благодаря за бързата и прецизна реакция — пуснах branch-а локално и го прегледах стриктно за сигурност и интегритет на данните; и двата блокера на @lyubomir-bozhinov са наистина затворени. Какво потвърдих локално (
Две малки, неблокиращи бележки:
Нито една от двете бележки не блокира merge. Заключение: Одобрявам — няма открити проблеми със сигурността или интегритета на данните; OWASP-съвместимо. Двете бележки по-горе са по желание. |
|
|
Потвърждавам за merge: двата предишни блокера са затворени ( |
|
Прегледах PR #169 стриктно и локално — за сигурност, интегритет на данните, SQL/injection вектори и злонамерен код. Пуснах тестовете сам и проверих всяко твърдение от дискусията вместо да го приема наготово. Обхват на промяната Какво потвърдих локално (
Заключение по OWASP: не откривам критични уязвимости; входната валидация, cache-ключването и DDL-ът са коректни и съвместими с OWASP Top 10. Двамата предходни рецензенти вече затвориха двата реални блокера; независимата ми проверка го потвърждава. Останалите бележки са по желание и не блокират merge. Вердикт: Одобрявам — няма открити проблеми със сигурността или интегритета на данните; OWASP-съвместимо. |
|
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). |
|
Verification complete. All checks pass on current HEAD ( Прегледах PR #169 стриктно и локално на текущия връх ( Обхват Какво потвърдих локално
Заключение по OWASP: не откривам критични уязвимости; cache-ключването, входната валидация и DDL-ът са коректни и съвместими с OWASP Top 10. Неблокиращи бележки (по желание):
Вердикт: Одобрявам — няма открити проблеми със сигурността или интегритета на данните; OWASP-съвместимо. Двете бележки по-горе не блокират merge. |
…ns, overrun index)
The popover often renders inside a thead th whose white-space: nowrap is inherited by every line of the card, so long summaries and the mono readout ran past the right edge. Reset wrapping on .metric-info-pop (white-space: normal + overflow-wrap: anywhere), widen the card to 320px clamped to the viewport (min(320px, 100vw - 16px)), and add the JS shift-into-viewport + coarse-pointer 44px hit area so all copies of the component behave identically.
|
@todorkolev готов за ревю 🙏 — rebase-нат на main, CI зелен, prettier-чист, CSS промените в styles/* (app.css само @import). Резолвнати нишки. Approve-ни когато ти е удобно. |
|
Прегледах PR #169 наново и стриктно — локално на текущия връх ( Обхват Какво потвърдих локално (
Съответствие с тикета: двата блокера на @lyubomir-bozhinov и моят предишен lens-блокер са затворени; независимата проверка го потвърждава. Обхватът е атомарен и подходящ за база на стек. Неблокираща бележка (по желание):
Заключение по OWASP: не откривам критични уязвимости; cache-ключването, входната валидация и DDL-ът са коректни и съвместими с OWASP Top 10. Вердикт: Одобрявам — няма открити проблеми със сигурността или интегритета на данните; OWASP-съвместимо. Бележката по-горе не блокира merge. |
|
Одобрявам. Ре-проверих стриктно на текущия връх
Без находки по базата на design-system-а. |
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
left a comment
There was a problem hiding this comment.
Преглед на 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-и и добре документиран код с коментари.
Основни забележки (не-блокиращи)
useLayoutEffectи SSR (MetricInfo.tsx): коментарът твърди „SSR-safe", ноuseLayoutEffectпредизвиква предупреждение при server render дори при раненreturn. Препоръка: isomorphic layout effect или обикновенuseEffect, тъй като изчислението и без това е клиентски път (приopen === true).- Остаряло изместване при
[open](MetricInfo.tsx): хоризонталният clamp се смята веднъж при отваряне; приresize/scrollдокато popover-ът е отворен стойността остарява и картата отново може да излезе извън екрана. Обмислете слушатели заresize/scrollдокато е отворен. aria-expandedспрямо hover/focus (MetricInfo.tsx): popover-ът се показва визуално чрез:hover/:focus-within, ноaria-expandedсе обновява само при click и сочи към постоянноaria-hidden="true"контейнер — семантично несъответствие. Влиянието е малко (текстът е вaria-label), но е добре да се изчисти.- Грешни CSS секционни маркери (
components.css): секцията отваря сtrends-dashboard, а затваря сend overruns-dashboard; маркерътend metric-info popoverреално затваря блокаlist-search. Козметично, но обърква навигацията. - Дублиран коментар (
pages.css, ред 869): коментарът за „Regional choropleth (/map)" е добавен наново, макар идентичен вече да съществува преди.map-wrap svg— премахнете единия. - Непоследователна
line-clampсъвместимост (pages.css):.clamp2включва и стандартнияline-clamp, но.az-card-desc,.pa-cardи.ov-annex-reasonразчитат само на-webkit-line-clamp— добавете и стандартното свойство. - Натрупване на
!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 проблеми. Изброените точки са подобрения по достъпност, коректност и козметика.
- 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
left a comment
There was a problem hiding this comment.
Преглед на 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)
FullscreenButtonбез consumer в тази серия файлове — по CLAUDE.md „NO DEAD CODE / NO PARTIAL IMPLEMENTATION“ това би било проблем, но е обозначен като умишлена база на stacked PR. Моля потвърдете, че consumer-ът идва в следваща партида.- a11y несъгласуваност:
aria-expandedвърху бутон, чийто popover еaria-hidden="true"и безaria-controls. - Отслабен drift-guard в
cache-key.test.ts: тестът „does not retain allow-list entries that nothing reads“ е сведен отexpect(stale).toEqual([])доconsole.info. Засяга само несигурностната посока (сигурностната остава твърдо асертирана) и е добре обоснован, но потвърдете, че е съзнателно и че over-keying на edge кеша е приемлив. - Дребно за производителност: capture-phase
scrolllistener може да еpassive: true.
Тестове
Тестовете за metric-info-clamp, MetricInfo, cache-key и миграцията са смислени (не тривиални). От предоставения diff не може да се потвърди зелен CI и покритие ≥90% за новия код — препоръчва се да се верифицира преди merge.
Заключение: код с високо качество, чист откъм сигурност. Одобрението зависи от потвърждаване на consumer-а за FullscreenButton, съзнателното отслабване на drift-guard теста, зелен CI/покритие и дребните a11y/производителностни бележки.
…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
left a comment
There was a problem hiding this comment.
Преглед на 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— математиката е вярна и покрита с тестове; съответства на CSSmax-width: min(320px, calc(100vw - 16px)).MetricInfo.tsx— внимателно управление на състояния, симетрични listener-и, rAF коалесциране, коректна достъпност (popoveraria-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 грижа (цветът не е единственият носител на смисъл).
Незадължителни бележки (не блокиращи)
- CSS дублиране —
.fs-btnvs.trend-fs-btn(партида 1) и двойно дефиниран.ov-board-scale(партида 2); обмислете обща база/сливане. FullscreenButtonбез call site — потвърдете, че свързването идва в следваща партида, за да не остане мъртъв код.query-params.ts— новите канонични параметри са безопасни срещу cache poisoning; единствен ефект е потенциална over-keying фрагментация. Уверете се, че drift guard-ът (cache-key.test.ts) действително покрива новите записи.- Козметика в стиловете — премахнат
outlineв.network-svg a:focus-visible(компенсиран със stroke пръстен — потвърдете контраста), смесени цветови пространства вcolor-mix(oklchvssrgb), нарастващ брой!important. - cache-key тест — бившата твърда проверка е превърната в „info“ тест, който винаги минава — гранично „cheater test“.
За потвърждение извън прегледа
- Реалното минаване на тестовете и покритието (не могат да се изпълнят тук).
- Твърдението „fully covering“ за overrun индекса зависи от
OVERRUN_WHEREвqueries/overruns.ts(друга партида). - Изричен тест за приложения
translate/shiftстил при видим popover би бил полезно допълнение.
Заключение: Чист, добре документиран design-system слой без проблеми по сигурност или данни. Забележките са за поддръжка и потвърждения — не блокират merge, но си струва да се адресират.
lyubomir-bozhinov
left a comment
There was a problem hiding this comment.
Ре-верифицирах на 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.
…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
… + 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>
|
Ъпдейт на миграционната номерация (замества по-ранната бележка за Стекът носи Значи номер (Странична бележка: euro-annex капанът вече е затворен на main чрез |
|
Този клон е в конфликт с |
ydimitrof
left a comment
There was a problem hiding this comment.
Преглед на 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-тата наистина не се използват никъде.
Основни находки (не блокиращи)
- Тестова покривка на toggle поведението (
MetricInfo.test.tsx/MetricInfo.tsx:142) — тестът проверява само класаis-openчрезfireEvent.click, който не генерира focus събития. В реален браузър:focus-withinдържи popover-а видим, така че видимото поведение остава непокрито. - Потенциално неизползван токен
--trend-count-ink— самият коментар признава, че още не се консумира; ако не се използва в останалите части, е dead code спрямо CLAUDE.md. - Неточен коментар в
migrations.test.ts— казва „0002", а индексът идва от миграция 0003. EXPECTED_STALE_PLANNED_PARAMSизключва записи (вкл. едносимволнитеa/b) от stale проверката — риск да маскира истински typo и да отслаби drift guard-а.- 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.
# 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
left a comment
There was a problem hiding this comment.
Обобщение на прегледа
Какво прави този 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-hiddenpopover, 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.
Одобрено, при условие че уточнението по теста за миграциите бъде потвърдено.
…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
left a comment
There was a problem hiding this comment.
Обобщение на прегледа
Какво прави този 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 съответства на CSSmax-width. - Миграция
0011: коректна и идемпотентна (IF NOT EXISTS), частичният индекс е подмножество на OVERRUN предиката, forward-only по конвенция; тестът проверява точната дефиниция (предикат + ред на колоните). - Зависимости:
@testing-library/reactи транзитивните ѝ записи вpnpm-lock.yamlса легитимни и съответстват на декларираната версия. - CSS/токени: следват конвенциите (
var(--...), без нови inlinestyle=); коментарите за WCAG 1.4.1 са уместни.
Блокиращи опасения
Няма. Одобрено и от трите партиди.
Дребни, необвързващи забележки
FullscreenButtonвсе още няма консуматор, а токенът--trend-count-inkи части като.fs-btn/.trend-fs-btnсе дублират/не се ползват — умишлена основа за следващи партиди на този stacked-PR.- Две дребни бележки по поддръжка в
pages.css. - Мъртъв код в един от тестовете (отбелязан инлайн).
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>
… номериране на миграциите, coverage baseline, споделени резолюции
What changed
Dashboard design-system base (MetricInfo popover, fullscreen toggle, design tokens, overrun index), now through four rounds of review fixes:
MetricInfopopover clamp recomputes from the unshifted rect (idempotent across repeated resize/scroll), re-triggers on hover/focus reveal (not just click), andaria-expanded/Esc now track hover+focus visibility, not justopen.MetricInfo: removed the strayaria-expandedfrom the trigger button — the popover it referenced is purely visual andaria-hidden, and its text is already conveyed viaaria-label, so the two were inconsistent.MetricInfo: the capture-phasewindowscrolllistener never callspreventDefault, so it's now registered{ passive: true, capture: true }(with matchingcapture: trueonremoveEventListenerso cleanup still detaches it).useFullscreen(zero call sites) removed as dead code;FullscreenButtonstays purely presentational — an intentional staged design-system base whose consumer (wiringactive/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— seeapps/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 incache-key.test.ts; only the reverse stale-entry direction is intentionally softened (see next bullet).trends-dashboard/overruns-dashboardsection-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.ts) draws its response-affecting param set from the sharedCANONICAL_QUERY_PARAMSsource of truth (CWE-349, issue Web cache poisoning: /contracts edge-cache key omits a response-affecting query parameter #56); confirmed thecohortrepeatable-param case is handled (URLSearchParamsiteration preserves every occurrence, not just the first).cache-key.test.ts: the removed hard-fail invariant is now an info-onlyconsole.info(stale allow-list entries are harmless — lower cache-hit-rate only, not correctness) with a comment explaining the tradeoff. Kept asconsole.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 anannex_count-only index perEXPLAIN QUERY PLANon a 190k-row fixture). No down-migration, matching this repo's forward-only convention (0000_init.sql,0001_flow_pairs_bidder_index.sqlhave none either;wrangler d1 migrationshas no rollback primitive) — documented in a migration comment rather than fabricating aDROP INDEX.migrations.test.tsstrengthened to assert the index's actual partial predicate and column order, not just its presence.MetricInfo.test.tsxadded — covers the open/hover/focus/dismissed state machine, Esc-close, and listener cleanup on unmount.--paper-raisedusesoklch(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 thearia-expandedremoval 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 newMetricInfo.test.tsxand the correctedmetric-info-clamp.test.ts"both edges clip" fixture.pnpm --filter @sigma/db test— 28 files / 199 tests passed, including the strengthenedmigrations.test.tsassertion on the overrun index's predicate/column order.tsc --noEmitclean for bothapps/webandpackages/db.Quality checks
IF NOT EXISTS).Вид промяна
fix— поправка на бъгfeat— нова функционалностdocs— документацияrefactor/perf/style— без промяна в поведениетоtest/ci/build/chore— поддръжкаЧеклист
Co-Authored-By:, вече публикувани, не се пренаписват) — виж „Бележка при merge“ по-долуmidt-bg/sigma:mainpnpm typecheckминава (CI: стъпка Typecheck е зелена)pnpm testминава (CI: стъпка Test е зелена)pnpm lintе чисто (CI: стъпка Lint е зелена).env*или.dev.vars(gitleaks + semgrep са зелени)docs/е обновена — не е приложимо за тази промянаБележка при merge
3 комита в този branch носят
Co-Authored-By:trailer към агент (Claude). AGENTS.md го забранява наmain, а squash-merge запазва trailer-ите от комитите. Историята не се пренаписва (без rebase/force-push), затова при merge моля заменете автоматично генерирания commit message със заглавието и тялото по-долу. Няма trailer-и към хора в този branch (всички комити са от автора на PR-а), така че не се губи credit.Заглавие (subject):
Тяло (body):