feat(web): прогноза в тренда (проекция по сезонен профил) - #192
feat(web): прогноза в тренда (проекция по сезонен профил)#192StanislavBG wants to merge 25 commits into
Conversation
Ревю на PR #192 — „прогноза в тренда (проекция по сезонен профил)"Благодаря за прегледната документация и за това, че прогнозата е внесена като отделна, включваема функция върху върха на стека. Прегледах кода изключително внимателно — с фокус върху сигурност, инжекции и цялост на данните — както ръчно, така и с два независими одиторски прохода (DB слой и уеб-маршрути). Работих локално върху Обхват и съответствие с описаниетоРеалната нова работа ( Сигурност (OWASP) — чисто
Цялост на данните
Незадължителни бележки (не блокиращи)
Процесна бележкаPR-ът е draft и diff-ът спрямо VERDICT: ОДОБРЯВАМ ✅ — сигурност и цялост на данните чисти; сливане само след стека #169→#172. |
…ns, overrun index)
The popover often renders inside a thead th whose white-space: nowrap is inherited by every line of the card, so long summaries and the mono readout ran past the right edge. Reset wrapping on .metric-info-pop (white-space: normal + overflow-wrap: anywhere), widen the card to 320px clamped to the viewport (min(320px, 100vw - 16px)), and add the JS shift-into-viewport + coarse-pointer 44px hit area so all copies of the component behave identically.
The обзор cross lens now re-runs the combo chart, year cards and totals server-side for the ticked CPV groups (repeatable ?cpv, validated 5-digit codes, deduped, capped at 10), and the chart stretches to fill its card against the CPV list. One aggregate scan via an OR of half-open cpv_code index ranges; ?cpv stays keyed in the edge cache (CWE-349) with UI-canonicalized ordering, and selection changes are announced via an sr-only status line.
The by-sector aggregate grouped on substr(t.cpv_code, 1, 2) and then ran cpvDivision() over the already-truncated prefix, so a dirty code like ' 45000000' resolved to division '4' there while the leaderboard (cpvDivision over the full code) resolved it to 45 — the same contract landed in different CPV sectors on the two surfaces. Select the full t.cpv_code, GROUP BY it in SQL, and run cpvDivision() on the full code in JS exactly like the leaderboard mapping; the existing merge folds all codes onto their canonical division and re-applies secLimit post-merge. Test covers a dirty leading char flowing through both surfaces to the same division.
…he full cpv_code Grouping by the full cpv_code fixed the dirty-code divergence but returned one row per distinct 8-digit code — 582 rows on the local corpus (thousands at scale) shipped out of D1 for a 15-row table. SECTOR_KEY_SQL strips the separator characters real codes carry and takes substr(clean, 1, 2) only when the cleaned code provably starts with two digits (in which case it IS cpvDivision(code)); anything else falls through as the full raw code for the JS re-key to fold in. Exact cpvDivision semantics, division-sized result set, still no pre-merge LIMIT truncation. Adds a sqlite3-CLI integration test pinning SECTOR_KEY_SQL ≡ cpvDivision across the dirt corpus, and a unit test pinning the GROUP BY key against both traps (naive substr, full-code blowup).
Every column header in „Кои институции раздуват най-много" and „Раздуване по сектори" now carries the same ⓘ MetricInfo popover the headline KPIs use: what the metric is, how it is computed (grounded in the SQL — growth is SUM(delta)/SUM(signing), €-weighted, not an average of percents), and the honest inclusion caveat (annex_count > 0, current > signing, signing ≥ €1 000). Table header cells get white-space: nowrap so the ⓘ never wraps the label. Also: the „Договори по мащаб" leaderboard rows gain right-side breathing room (row padding-right var(--s-3)) so value text and truncated titles no longer touch the card edge; the inset lives on the row, so the selected highlight still reads full-width and the proportional bar-track math is untouched in normal and fullscreen mode.
…compare lens (review)
6714063 to
1ce433c
Compare
1ce433c to
90a734e
Compare
|
Ревю на PR #192 — „прогноза в тренда (проекция по сезонен профил)" Благодаря за изчерпателното описание и за дисциплината прогнозата да е внесена като отделна, включваема функция върху върха на стека. Прегледах кода изключително внимателно, с акцент върху сигурност, инжекции и цялост на данните — ръчно и с локални проверки върху Обхват и съответствие с описанието Сигурност (OWASP) — чисто
Цялост на данните
Незадължителни бележки (не блокиращи)
Процесна бележка: PR-ът е draft и стъпва върху #169→#172. Да не се un-draft-ва/слива преди основния стек — както е записано в описанието. ВЕРДИКТ: ОДОБРЯВАМ ✅ — по същество; сигурност и цялост на данните чисти. Сливане само след стека #169→#172. |
|
Ревю на PR #192 — „прогноза в тренда (проекция по сезонен профил)" Благодаря за изрядната документация и за дисциплината прогнозата да влезе като отделна, включваема функция върху върха на стека (#169→#172), държана извън опашката по продуктово решение. Прегледах кода много внимателно — ръчно и с фокус върху сигурност, инжекции и цялост на данните — локално върху Обхват и съответствие с описаниетоРеалната нова работа отговаря точно на описания метод. Сигурност (OWASP) — чисто
Цялост на данните
Дребни бележки (не блокиращи)
Няма следи от злонамерен код, скрити мрежови повиквания, четене на secrets или обфускация в диапазона на промяната. Вердикт: Approve на същество — да се un-draft-не и слее след като стекът #169→#172 влезе в |
|
Финалната проверка е готова. Тестовете не тръгват в изолирания worktree само защото workspace-пакетите ( Ревю на PR #192 — „прогноза в тренда (проекция по сезонен профил)"Здравейте! Прегледах промяната изключително внимателно, с акцент върху сигурност, целостта на данните и потенциален зловреден или инжекционен код. По-долу са изводите. Обхват и съответствие с описаниетоРеализацията отговаря точно на описанието в PR-а:
Стекова зависимост (#169 → #170 → #171 → #172, член 5) е коректно декларирана; draft-ът се държи извън опашката по продуктово решение и следва да се un-draft-не и слее след стека — не преди. Сигурност / OWASP
Цялост на даннитеИзчислението е коректно защитено:
Дребни бележки (незадължителни)
ЗаключениеЧиста, добре тестирана и честна към данните функция; без проблеми по сигурност или цялост. Единственото условие е редът на сливане: само след стека #169–#172 и un-draft. ВЕРДИКТ: Approve на същество — да се слее след стека #169–#172; без блокиращи забележки. |
|
Одобрявам прогнозата ( Cross-PR: #193 (methodology) описва /trends като „Няма прогноза — само реални стойности" ( |
|
Closing — the seasonal-forecast projection isn't something we want to ship. Dropping this feature rather than carrying it forward as dormant draft work. |
Държи се извън опашката за ревю по продуктово решение — графиката в основния стек (#169–#172) показва само реални данни. Този draft пренася прогнозата като отделна, включваема функция; готов за un-draft след като стекът се слее.
Зависимости
Базиран върху върха на стека: #169 → #170 → #171 → #172 (член 5 на стека). Да не се ревюира преди тях.
Метод (накратко)
estimateYoyGrowth— медиана на съотношенията на последните 3 пълни години, clamp срещу еднократни аномалии в корпуса). Нищо не е фабрикувано — всичко се извежда от реалните месечни серии.Обхват
apps/web/app/lib/trends-forecast.ts(+ тестове) —buildForecast, преизползваestimateYoyGrowthотanalytics-statsComboTrendChart— прогнозен слой (регион, линия, барове, tooltip badge)/trends— прогноза само на месечната стъпка на лещата „Във времето" + легенда