Wire analysis/[cad]/page.tsx: replace SidebarPlaceholder with
AnalysisSidebar (sticky, IntersectionObserver scrollspy on sections
1/2/3/3.1/3.2/3.3/4/5), header with AnalysisBreadcrumb (SiteFinder
→ cad → Анализ) and UserAvatar (gd_org_id from localStorage, SSR-safe).
Add lucide-react to package.json (was used in A1 but missing from deps).
When Objective mapping coverage falls below 50% of competitors in the radius,
fall back to rosreestr_deals JOIN on the parcel's cadastral quarter. Audit shows
237/237 EKB quarters (100%) have rosreestr data for the last 12 months, compared
to <20% Objective coverage before bulk mapping.
- velocity.py: add _compute_rosreestr_fallback(), _OBJECTIVE_COVERAGE_MIN_RATIO
constant, velocity_source field on VelocityResult (objective/rosreestr_fallback/none)
- parcels.py: extract cad_quarter from cad_num, pass to compute_velocity
- site-finder.ts: add velocity_source field to Velocity interface
- VelocityBlock.tsx: badge "Источник: квартальные сделки" when rosreestr_fallback
Epic #271 item #17
- v_bucket_success_score HAVING >= 30 → >= 15 so sparse districts
(Кировский etc.) are no longer silently excluded from the block
- Backend: SUCCESS_REC_MIN_DEALS=15, SUCCESS_REC_STRONG_DEALS=30;
data_confidence='weak' (15-29 deals) or 'strong' (≥30) in response
- TS type ParcelSuccessRecommendation: add data_confidence literal
- SuccessRecommendationBlock: amber badge for weak, all hex → CSS tokens,
emoji star removed (ui-ux.md), empty-state text min 30→15
Add «Срок продажи (мес)» column (supply / velocity, colored by urgency)
and click-to-expand drill-in row showing competitor_obj_ids as DOM.РФ
linked badges. Both fields were already present in TopLayoutRow type.
Closes#271 item 11
Replace INNER JOIN on objective_complex_mapping with LEFT JOIN approach:
competitors without mapping now return velocity=0 with
velocity_data_available=False instead of being silently dropped.
UI: VelocityBlock shows 'нет данных velocity' badge when flag is False
and hides the gauge (meaningless zero). TS type updated (additive optional
field, backward compat).
Audit (EKB, region_cd=66): mapped=129, unmapped=1387, total=1516.
Closes#271 item #10
_build_recommendation зеркалило рынок через чистый Hamilton apportionment.
Добавлен _cap_and_redistribute: bucket >35% clamp к 35%, surplus
пропорционально перераспределяется в остальные bucket'ы (Hamilton финальный
pass гарантирует sum=100). Pathological case (все bucket'ы >35%) — warning
в лог, cap пропускается. Frontend: warning banner при maxPct>60 в
RecommendationCard (Tailwind amber utilities, без inline hex).
ekb_districts.median_price_per_m2 was populated from a 24-month window.
LEFT JOIN mv_quarter_price_per_m2 on the cad quarter and COALESCE(median_12m,
fallback) so the API returns the 12-month median where available.
Frontend OverviewTab adds a "(12 мес)" hint next to the value.
Closes#271 item 7
Marketing landing «gendsgn — аналитика спроса для девелоперов».
Self-contained HTML (50KB, inline assets) — будет доступен через
Next.js public/ serving по URL https://gendsgn.ru/landing.html
После backend PR #276 (site_status в /analyze) — frontend показывает
статус строящиеся/сданные с цветным badge и фильтр-tabs наверху таблицы.
TS type extended manually (backward compat) — codegen подхватит после
backend deploy.
Closes (epic part) #271 item 3
Symptom: sold_pct_of_supply = sum_deals_24mo / supply_count_snapshot * 100
yields >100% (e.g. 199%) for fast-selling small formats due to incompatible
time windows (24-month deals vs point-in-time supply snapshot).
Option A (hotfix): clamp to 100.0, expose is_oversold=True for UI badge.
- best_layouts.py: compute sold_pct_raw, clamp with min(..., 100.0),
set is_oversold = raw > 100%
- parcel.py TopLayoutRow: add is_oversold: bool field
- best-layouts.ts TopLayoutRow: add is_oversold: boolean
- BestLayoutsBlock.tsx: show warn badge ">100%" when is_oversold=True
- tests: two new cases — raw 199% clamps + is_oversold=True;
raw 50% passes through + is_oversold=False
Closes (epic part) #271 item 5
apiFetch и apiFetchWithStatus не передавали X-Session-Id header, из-за
чего POST /analyze возвращал custom_poi_score_items: [] и score без учёта
пользовательских POI. Добавлен withSessionHeader() helper с SSR guard;
user-supplied headers имеют приоритет (backward compat).
Backend (quarter_dump_lookup.py):
- _acquire_harvest_lock: Redis SETNX TTL=120s на quarter, защищает от burst
N concurrent analyze, ставящих N одинаковых harvest task в очередь
- _trigger_harvest: использует lock перед apply_async, возвращает False если
lock уже взят (другой запрос триггернул раньше)
- make_empty_result/EMPTY_DUMP_RESULT: новое поле harvest_eta_seconds в
nspd_dump dict, типичный harvest_quarter = 60с
- /analyze: пробрасывает поле через nspd_dump dict (нет typed schema —
response_model=None для /analyze endpoint, dict уходит как есть)
Frontend (NspdFreshnessBadge, NspdZoningBlock):
- Countdown «НСПД: загрузка ~Nс» вместо бесконечного спиннера
- После остановки countdown (remaining=0) NspdZoningBlock показывает
«загрузка дольше обычного» + ссылку на ПКК вместо infinite skeleton
Tests: 5 новых unit + 2 для empty_result schema (всего +7, pass)
Closes#234 (UX-side; data-side resolves когда Sub-PR B + D merged).
Problem
- GlitchTip Issues = 0 за всё время, хотя backend + frontend SDK интегрированы (PR #207, #208).
- Старый Sentry.io продолжает получать события — user видит уведомления оттуда.
Root cause
- frontend/Dockerfile не имеет ARG NEXT_PUBLIC_GLITCHTIP_DSN → `npm run build` бьёт
с пустым env var → Next.js инлайнит undefined → SDK init guard `if (dsn)` skips.
Chrome-devtools check на prod bundle подтвердил: ни в одном из 9 chunks DSN-строка
не запечена; `i.env.NEXT_PUBLIC_GLITCHTIP_DSN` evaluates to undefined.
- .forgejo/workflows/deploy.yml build-frontend не передавал build-args.
- На VPS backend/.env.runtime содержит legacy SENTRY_DSN=...@sentry.io/...
config.py:_promote_legacy_sentry_dsn слепо промоутит его в glitchtip_dsn → SDK
шлёт в чужой Sentry. GLITCHTIP_DSN там не задан.
- deploy.yml SSH-скрипт никогда не редактировал SENTRY_DSN/GLITCHTIP_DSN в .env.runtime.
Solution
1. frontend/Dockerfile: ARG NEXT_PUBLIC_GLITCHTIP_DSN + NEXT_PUBLIC_ENVIRONMENT
с пустыми defaults, ENV-mirror перед `npm run build`. Локальный build без
build-args работает по-прежнему (no-op DSN).
2. .forgejo/workflows/deploy.yml:
- build-frontend: `build-args` передаёт `secrets.GLITCHTIP_FRONTEND_DSN`
+ NEXT_PUBLIC_ENVIRONMENT=production. Инвалидирует cache → frontend
image пересоберётся (expected).
- deploy step: GLITCHTIP_BACKEND_DSN через secret, в SSH-скрипте:
a) `sed -i '/^SENTRY_DSN=/d' backend/.env.runtime` — снести legacy
b) upsert GLITCHTIP_DSN (sed/printf) тем же паттерном что SENTRY_RELEASE
c) `compose up -d --force-recreate --no-deps backend worker beat` —
обычный `up -d` не перечитывает env_file без image change.
3. backend/app/core/config.py: _promote_legacy_sentry_dsn ужесточён —
принимает SENTRY_DSN только если host == errors.gendsgn.ru. Для других
URLs (sentry.io) выдаёт UserWarning и НЕ промоутит. Anti-regression на
случай если SENTRY_DSN снова окажется в .env.runtime после ручного
вмешательства.
Required Forgejo secrets (Settings → Actions → Secrets)
- GLITCHTIP_BACKEND_DSN = https://3d6e291003e142458957490c83559867@errors.gendsgn.ru/1
- GLITCHTIP_FRONTEND_DSN = https://5d7bc85e300c4e80a8554ccc818ff56d@errors.gendsgn.ru/2
DSNs публичны (видны в browser bundle) — secrets ради build-time injection,
не для конфиденциальности. Если secrets не заданы → deploy succeeds, SDK
no-op, без регрессии.
Test plan
- Verify Forgejo deploy.yml зелёный после merge
- chrome-devtools: открыть gendsgn.ru → search bundle на DSN string → должна
быть запечена строка errors.gendsgn.ru/2
- Trigger frontend error → POST к errors.gendsgn.ru/api/2/envelope/
- Backend: curl на endpoint вызывающий 500 → событие в GlitchTip backend project
- GlitchTip dashboard https://errors.gendsgn.ru/gendesign/issues — Issues > 0
References
- vault: meta/00_credentials.md (DSNs + incident notes 2026-05-16)
- vault: decisions/Dec_GlitchTip_Frontend_Sentry_SDK.md (env contract)
- vault: fixes/fixes-MOC.md (#204 backend SDK init)
Add admin page for triggering and monitoring cadastre_jobs:
- Trigger form: pilot (50) / ekb_full (2408) / manual_list scopes
- Live jobs table 5s auto-refresh (TanStack Query)
- Per-row expand: phase_state JSON, req/s rate, WAF count, error
- Cancel button for queued/running, Resume for failed/cancelled/paused
- Heartbeat staleness: red "STALE" if running and gap > 10min
- WAF count highlighted red+⚠ when > 5
- Progress bar from targets_done/targets_total
- New tab in admin layout
- Token in localStorage same as geo page
Co-authored-by: lekss361 <claudestars@proton.me>
Bars rendered as ~1-15px because outer flex container had
align-items:flex-end which made each column shrink to label
height (~15.8px), so bar percentage heights resolved against
that instead of the intended 80px container.
Switched to align-items:stretch + fixed-pixel bar heights
anchored to a 60px area (reserves ~20px for label row). Each
column now uses justify-content:flex-end so bars + labels sit
at the bottom baseline.
Co-authored-by: lekss361 <claudestars@proton.me>