956e461b9f
Merge pull request 'feat(sf-fe-a4): analysis sidebar (scrollspy) + breadcrumb + user avatar' ( #344 ) from feat/sf-fe-a4-sidebar into main
Deploy / changes (push) Successful in 7s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / deploy (push) Has been skipped
Deploy / build-frontend (push) Failing after 26s
2026-05-17 22:20:03 +00:00
5aab8a97af
Merge pull request 'feat(sf-fe-a2): /site-finder entry — EntryMap + filters + drawer + recent' ( #343 ) from feat/sf-fe-a2-entry into main
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Has been cancelled
2026-05-17 22:19:17 +00:00
lekss361
0084773abf
feat(sf-fe-a2): EntryMap + MapFilterBar + ParcelDrawer + RecentParcels + ParcelLegend
...
- EntryMap: Leaflet карта ЕКБ с CircleMarker парцелей, bbox-aware refetch (TanStack Query),
click-to-select, deselect on backdrop, isFetching indicator
- MapFilterBar: chip-фильтры (status/area/vri) + district select + counter, bbox-linked
counter через FilterBarBridge без ремаунта карты
- ParcelDrawer: slide-in panel 360px — 4 KPI (status/area/district/vri) + coords +
CTA «Открыть анализ» -> /site-finder/analysis/[cad], fallback ПКК Росреестр
- RecentParcels: TanStack Query hook с localStorage fallback (gd_recent_parcels),
merges B2 server list + local-only items; empty state + hover effects
- ParcelLegend: status color legend (free/in_progress/favorite) + STATUS_COLORS export
reused in EntryMap markers
- site-finder-api.ts: useParcelsBboxQuery + useRecentParcels hooks, mock fallback via
MOCK_PARCELS_BBOX / MOCK_RECENT_PARCELS; getLocalRecentParcels / addLocalRecentParcel
- parcels-bbox.json: 20 ЕКБ-парцелей фикстура (6 районов, все статусы, разные ВРИ)
- page.tsx: заменены MapPlaceholder + SidebarPlaceholder на реальные компоненты
2026-05-18 01:12:32 +03:00
lekss361
440b53cef0
feat(sf-fe-a4): AnalysisSidebar (scrollspy) + Breadcrumb + UserAvatar
...
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).
2026-05-18 01:12:18 +03:00
2548f71f40
feat(sf-fe-a3): UI primitives — HeadlineBar + Badge + Drawer ( #341 )
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Failing after 1m6s
Deploy / deploy (push) Has been skipped
2026-05-17 22:06:53 +00:00
3aaaaba5f9
feat(sf-fe-a1): routes shell — entry / analysis + _legacy/ backup + mock toggle ( #340 )
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Has been skipped
Deploy / deploy (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Failing after 1m18s
2026-05-17 22:01:49 +00:00
3978aa5905
feat(trade-in): TI-2 PDF export 4-page + frontend enable button ( #319 )
Deploy / changes (push) Successful in 5s
Deploy / build-worker (push) Successful in 2m40s
Deploy / build-backend (push) Successful in 1m24s
Deploy / build-frontend (push) Successful in 2m58s
Deploy / deploy (push) Successful in 51s
2026-05-17 17:16:48 +00:00
90eb5af883
feat(trade-in): TI-3 frontend /trade-in page + 5 components + hooks ( #317 )
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / deploy (push) Successful in 39s
Deploy / build-frontend (push) Successful in 2m45s
2026-05-17 16:58:53 +00:00
f8c382f885
feat(22k): object page — full exposure 9 blocks (sales/quartirography/metro/photos/docs/specs/checks) ( #310 )
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 1m35s
Deploy / build-worker (push) Successful in 2m37s
Deploy / build-frontend (push) Successful in 3m2s
Deploy / deploy (push) Successful in 56s
2026-05-17 15:56:39 +00:00
a86cce79af
refactor(sf-19): BestLayouts inline hex → Tailwind tokens ( #296 )
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Has been cancelled
2026-05-17 14:17:05 +00:00
4b967343f8
fix(sf-13): MarketTab above-the-fold headline + 3 KPI cards ( #294 )
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 2m53s
Deploy / deploy (push) Successful in 49s
2026-05-17 14:10:06 +00:00
lekss361
e1a8ad9395
fix(sf-17): rosreestr velocity fallback via cad_quarter (~100% EKB coverage)
...
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
2026-05-17 16:50:19 +03:00
9eed55cd30
Merge pull request 'fix(sf-20): adaptive district threshold 30→15 with data_confidence flag' ( #288 ) from fix/sf-20-district-threshold-adaptive into main
Deploy / changes (push) Successful in 4s
Deploy / build-backend (push) Successful in 1m49s
Deploy / deploy (push) Successful in 1m0s
Deploy / build-worker (push) Successful in 2m41s
Deploy / build-frontend (push) Successful in 3m9s
2026-05-17 13:39:52 +00:00
lekss361
989d4e5b7d
fix(sf-20): adaptive district threshold 30→15 with data_confidence flag
...
- 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
2026-05-17 16:34:52 +03:00
lekss361
1e1737e829
fix(sf-12): MarketTrend — adaptive n<30/n<100 confidence warnings
...
n<30: hide trend block, show 'insufficient data — expand radius'.
30≤n<100: show trend + amber banner 'weak data (n=X) — expand radius'.
n≥100: unchanged, no banner.
2026-05-17 16:33:22 +03:00
lekss361
1959283a41
fix(sf-11): review-pass — DOM.РФ latin URL + Fragment key + a11y keyboard
...
Per review #285 (HIGH BLOCK):
- DOM.РФ URL: кириллический slug → latin path
(/services/catalog-newbuildings/object/{id}) для stable behavior
- Replace <> Fragment без key → <Fragment key={signature}>
(убирает React warning при .map). Closing </> → </Fragment>
- a11y: <tr onClick> теперь имеет tabIndex, role="button",
aria-expanded, onKeyDown (Enter/Space) — keyboard drill-in
Inline hex tokens (medium) — отложено в follow-up (epic #271 #19 ).
2026-05-17 16:16:24 +03:00
lekss361
d3a51c3657
fix(sf-11): Top layouts — sales period column + drill-in expand
...
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
2026-05-17 16:07:30 +03:00
lekss361
b7ce4a589c
fix(sf-10): velocity LEFT JOIN + velocity_data_available flag
...
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
2026-05-17 15:51:43 +03:00
lekss361
bcd7dc8f75
fix(sf-09): iterative cap algorithm + cap_skipped flag + unit tests
...
Review verdict MINOR (PR #282 ) — single-pass cap_and_redistribute
не гарантировал invariant max ≤ MAX_BUCKET_SHARE_PCT. Surplus от clamped
поднимал free выше cap (например {1k:75, studio:15, 2k:10} → studio=39).
- backend/app/services/site_finder/best_layouts.py
- _cap_and_redistribute → iterative while-loop до сходимости
- Returns (result_map, cap_skipped) — флаг для pathological all-clamped
- Float work map, Hamilton финальный pass для invariant sum=100
- backend/app/schemas/parcel.py
- LayoutTzRecommendation.cap_skipped: bool = False
- backend/tests/services/site_finder/test_best_layouts.py
- 6 parametrized tests: invariants, 7 failing cases from review,
no-dominant unchanged, empty, cap_skipped propagation, normal-case
- frontend/src/types/best-layouts.ts — cap_skipped: boolean
- frontend/src/components/site-finder/BestLayoutsBlock.tsx
- banner условие сменилось с maxPct > 60 на rec.cap_skipped (honest signal)
2026-05-17 15:21:51 +03:00
lekss361
0d11f426b2
fix(sf-09): MAX_BUCKET_SHARE 35% cap + frontend warning banner
...
_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).
2026-05-17 14:49:56 +03:00
lekss361
4d39ae5b5d
fix(sf-07): use median_12m from mv_quarter_price_per_m2 for district price
...
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
2026-05-17 14:28:32 +03:00
102ef88d56
Merge pull request 'fix(sf-03): CompetitorTable Status column + filter tabs' ( #278 ) from fix/sf-03-competitor-table-status into main
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 2m58s
Deploy / deploy (push) Successful in 48s
2026-05-17 10:50:27 +00:00
lekss361
98323027aa
fix(sf-03): CompetitorTable Status column + filter tabs
...
CI / backend (pull_request) Failing after 49s
CI / frontend (pull_request) Successful in 1m55s
После backend PR #276 (site_status в /analyze) — frontend показывает
статус строящиеся/сданные с цветным badge и фильтр-tabs наверху таблицы.
TS type extended manually (backward compat) — codegen подхватит после
backend deploy.
Closes (epic part) #271 item 3
2026-05-17 13:41:15 +03:00
ac1da79946
Merge pull request 'fix(sf-05): clamp sold_pct_of_supply на 100% + is_oversold flag' ( #274 ) from fix/sf-05-sold-pct-normalize into main
Deploy / changes (push) Successful in 4s
Deploy / build-backend (push) Successful in 1m32s
Deploy / build-worker (push) Successful in 2m39s
Deploy / build-frontend (push) Successful in 3m3s
Deploy / deploy (push) Successful in 55s
2026-05-17 09:04:21 +00:00
lekss361
247c252b04
fix(sf-05): clamp sold_pct_of_supply at 100% + add is_oversold flag
...
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
2026-05-17 11:57:29 +03:00
lekss361
81bf798c27
fix(sf-06): MarketTab reorder — Competitors → BestLayouts → Velocity первыми
2026-05-17 11:52:45 +03:00
lekss361
2d58c3a203
fix( #264 ): apiFetch — global X-Session-Id header (custom POI scoring в UI)
...
apiFetch и apiFetchWithStatus не передавали X-Session-Id header, из-за
чего POST /analyze возвращал custom_poi_score_items: [] и score без учёта
пользовательских POI. Добавлен withSessionHeader() helper с SSR guard;
user-supplied headers имеют приоритет (backward compat).
2026-05-17 10:31:54 +03:00
e3eb8c873f
Merge pull request 'feat(#254,#114): custom POI UI (Leaflet click-to-add) + score breakdown stacked-bar' ( #258 ) from feat/254-custom-pois-ui into main
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 3m2s
Deploy / deploy (push) Successful in 39s
2026-05-17 06:47:08 +00:00
lekss361
6ecd7a9269
feat(#254,#114): custom POI UI + score breakdown stacked-bar viz
...
Task A (#254 ): Leaflet click-to-add custom POIs
- sessionId.ts: getOrCreateSessionId() from localStorage
- types/customPoi.ts: CustomPoi, CustomPoiCreate, CustomPoiUpdate
- hooks/useCustomPois.ts: useCustomPois / useAddCustomPoi / useUpdateCustomPoi / useDeleteCustomPoi (TanStack Query, X-Session-Id header)
- CustomPoiLayer.tsx: LayerGroup with CircleMarker (green/red/gray by weight), popup with edit/delete
- CustomPoiToggleButton.tsx: crosshair mode toggle button
- CustomPoiAddModal.tsx: modal form (name, 26 categories, weight -5..+5, notes)
- CustomPoiEditModal.tsx: same form for editing existing POI
- SiteMap.tsx: MapClickHandler (useMapEvents), integrated all custom POI components
- page.tsx: useCustomPois hook + passes customPois + parcelCad to SiteMap
Task B (#114 ): ScoreBreakdownStackedBar
- ScoreBreakdownStackedBar.tsx: horizontal stacked bar by category with positive/negative split, toggle between category and factor views, custom POIs highlighted orange with dashed outline
- OverviewTab.tsx: renders ScoreBreakdownStackedBar below ScoreBreakdownPanel
Requires backend PR with /api/v1/custom-pois endpoints merged before POI CRUD works.
2026-05-17 09:35:35 +03:00
lekss361
990c109756
fix( #234 ): NSPD harvest ETA badge + SETNX dedupe (Sub-PR C)
...
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).
2026-05-17 09:13:06 +03:00
lekss361
a7671513c9
feat( #115 ): Leaflet layer toggle для connection points (Макс KILLER)
...
- ConnectionPointsLayer.tsx — per-category CircleMarker слой внутри MapContainer
(electricity/gas/water/heat/sewage/telecom/other, классификация по keywords)
- CpLayerControlPanel.tsx — toggle panel под картой: checkbox per-category + count,
toggle-all, summary badges (ближайший, охранная зона)
- SiteMap.tsx — принимает connectionPoints?: ConnectionPointsResponse,
управляет visibleCategories state, рендерит оба новых компонента
- page.tsx — useConnectionPoints(data?.cad_num) -> передаёт в SiteMap
- Empty-state: "0 точек подключения" при dump_available=false или пустом ответе
2026-05-16 22:51:12 +03:00
5a7d405c77
Merge pull request 'feat(nspd): TIER 4 opportunity + red lines ( #94 PR2 of 4)' ( #220 ) from feat/94-pr2-opportunity-redlines into main
Deploy / changes (push) Successful in 4s
Deploy / build-backend (push) Successful in 1m30s
Deploy / build-worker (push) Successful in 2m44s
Deploy / build-frontend (push) Successful in 3m10s
Deploy / deploy (push) Successful in 41s
2026-05-16 17:09:52 +00:00
lekss361
fdb54834e5
fix(nspd): rename migration 89→98, fix red lines ST_Area→ST_Length ( #220 )
...
- data/sql/98_*: rename from 89 to avoid collision with existing 89_drop_dead_brin
- _get_red_lines: ST_Length(ST_Intersection(planar)::geography) instead of
ST_Area(ST_Intersection(::geography, ::geography)) — fixes PostGIS 3.4
tolerance error and returns correct non-zero length for LINESTRING intersections
- Rename intersection_area_sqm → intersection_length_m across schema, TS types,
frontend component, and tests; add test_get_red_lines_db_exception_returns_empty
Addresses review-bot feedback on PR #220 .
2026-05-16 19:33:14 +03:00
lekss361
ed3c128528
feat(nspd): TIER 4 opportunity layers + red lines ( #94 PR2 of 4)
...
- NSPDClient: QUARTER_OPPORTUNITY_LAYERS (auction/scheme/free/future/oopt)
+ QuarterDump.opportunity field
- nspd_sync: harvest_quarter accepts include_opportunity, denorm cols
has_auction_parcels + opportunity_count in UPSERT
- quarter_dump_lookup:
- _get_opportunity_parcels (sort by distance, early-exit on count=0)
- _get_red_lines (query existing dump.red_lines core path, layer='red_lines')
- SQL 89_*: has_auction_parcels + opportunity_count + partial index
- Pydantic: OpportunityParcel + RedLine schemas
- /analyze: nspd_opportunity_parcels + nspd_red_lines fields
- Frontend: NspdOpportunityBlock + NspdRedLinesBlock + LandTab integration
- Tests: 28 total (11 PR1 + 17 PR2), all pass
Red lines uses existing core harvest path (layer 879243 already in dump.red_lines
from PR1+core) — no duplicate harvest, no red_lines_count_v2 column needed.
Part of #94
2026-05-16 19:06:22 +03:00
277499684b
feat(frontend): @sentry/nextjs init для GlitchTip ( #204 frontend) ( #208 )
Deploy / changes (push) Successful in 4s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 4m17s
Deploy / deploy (push) Successful in 31s
2026-05-16 15:09:38 +00:00
lekss361
1595157ebf
feat(nspd): enable TIER 3 risk zones harvest + extract in analyze ( #94 PR1)
...
- harvest_stale_quarters fanout passes include_risks=True to all queued tasks
- _get_risk_zones() with ST_Intersects + ST_Area intersection calc
- _extract_features_by_layer() generic helper for future PR2+ reuse
- RiskZone Pydantic schema; /analyze returns nspd_risk_zones list
- NspdRiskZonesBlock component with severity color-coding (red/yellow/orange)
- 11 unit tests for extract/intersect/null-area/db-exception paths
Part of #94 (TIER 3 risk layers — flooding, landslide, burns, erosion etc)
2026-05-16 16:11:59 +03:00
ce2c48fff5
feat(site-finder): debounce weight re-analyze ( #201 Phase 2) ( #215 )
Deploy / build-frontend (push) Successful in 2m0s
Deploy / deploy (push) Successful in 28s
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
2026-05-16 12:54:51 +00:00
e450546178
feat(site-finder): NSPD frontend integration в LandTab ( #202 ) ( #212 )
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Successful in 1m58s
Deploy / deploy (push) Successful in 29s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
2026-05-16 11:55:31 +00:00
092976f656
feat(site-finder): inline POI weights pass-through в /analyze ( #201 Phase 1) ( #206 )
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 1m35s
Deploy / build-worker (push) Successful in 2m44s
Deploy / build-frontend (push) Successful in 2m9s
Deploy / deploy (push) Successful in 37s
2026-05-16 11:00:41 +00:00
256909d28b
feat(layouts): PDF ТЗ endpoint + BestLayoutsBlock UI ( #113 PR D) ( #199 )
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 1m38s
Deploy / build-worker (push) Successful in 3m3s
Deploy / build-frontend (push) Successful in 2m8s
Deploy / deploy (push) Successful in 38s
2026-05-16 09:53:05 +00:00
lekss361
ced237804d
feat(admin): cadastre bulk harvest UI at /admin/scrape/cadastre ( #168 PR4/5) ( #172 )
...
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>
2026-05-15 13:46:07 +03:00
lekss361
b4327635fa
fix(site-finder): visible by-quarter pipeline bars ( #167 )
...
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>
2026-05-15 10:35:51 +03:00
lekss361
faa99abb94
feat(site-finder): per-room bucket breakdown в VelocityBlock ( #166 )
...
VelocityBucketStat + optional by_room_bucket fields.
'По комнатности' aggregate table + per-competitor inline mini-row.
Backward compat via optional field presence guards.
Closes #161 (frontend part)
Co-authored-by: lekss361 <claudestars@proton.me>
2026-05-15 10:12:42 +03:00
lekss361
8fa1d005fd
feat(velocity): migrate D2 sales source → objective_corpus_room_month ( #158 )
...
* feat(velocity): migrate D2 sales source from stale sale_graph to objective_corpus_room_month
Closes #156
* fix(velocity): cross-stack contract + NULL safety per pre-push review
3 pre-push code-reviewer findings fixed:
1. TS union extended ('objective'|'sale_graph'|'rosreestr_fallback')
+ UI conditions handle both objective и sale_graph как valid sources.
2. COALESCE(deals_total_vol_m2, deals_total_count * 45.0) — NULL safety
for DKP-only rows (vol_m2 nullable, count > 0).
3. room_bucket parking filter — verified false positive (все 5 buckets
apartments: студия/1/2/3/4+).
Refs: PR #157 pre-push code-reviewer
* fix(types): add 'objective' to velocity_source union (cherry-pick miss)
Cherry-pick от закрытого PR #157 потерял этот файл. Frontend type-check fails
с 'no overlap' error. Restoring.
Refs: PR #158 frontend CI failure
---------
Co-authored-by: lekss361 <claudestars@proton.me>
2026-05-15 09:34:00 +03:00
lekss361
142ed95d36
fix(frontend): apiFetchWithStatus body stream double-read crash ( #149 )
...
Per user report 2026-05-14: 'Failed to execute text on Response: body stream
already read' при analyze с non-JSON error response.
apiFetchWithStatus делал .json() (consumes stream), потом .text() в catch
(FAILS — stream already consumed). Fetch API: body — ReadableStream,
consumable один раз.
Fix: read text() once → JSON.parse on string. На fail (HTML / non-JSON)
оборачиваем в {detail: rawText}. Никаких double-reads.
Site-finder cad search больше не крашится на error pages.
Co-authored-by: lekss361 <claudestars@proton.me>
2026-05-15 07:39:21 +03:00
lekss361
088c1ab2cf
feat(site-finder): VelocityBlock в MarketTab ( #34 sub-PR 2/2 FINAL) ( #147 )
...
* feat(site-finder): VelocityBlock в MarketTab (#34 sub-PR 2/2 FINAL)
D2 velocity-score frontend: score gauge, period meta, top 5 competitors table.
TS types (Velocity/VelocityCompetitor/VelocityPeriod) добавлены.
Guard "velocity" in data для backward-compat pre-#146.
tsc 0 errors, ESLint 0 warnings.
Closes #34
* fix(site-finder): align VelocityBlock TS types with backend response shape
Per #147 bot review — cross-stack contract mismatch:
Backend velocity.py (PR #146 merged) returns:
obj_id, name, dev_name, obj_class, district_name, distance_m, total_sqm_period
Frontend types had:
obj_id, name, class, distance_m, total_sqm
Result: c.class undefined → never showed class chip; c.total_sqm undefined →
Math.round(NaN) = NaN, 'не число' в table column для всех 5 строк.
## Fix
- VelocityCompetitor.class → obj_class
- VelocityCompetitor.total_sqm → total_sqm_period
- Added optional dev_name, district_name fields
- Updated VelocityBlock.tsx — все 3 references (guard, chip render, table cell)
Refs: #34 , #147
---------
Co-authored-by: lekss361 <claudestars@proton.me>
2026-05-15 01:44:16 +03:00
lekss361
95647a1c82
feat(site-finder): G5 gate verdict banner in OverviewTab ( #32 sub-PR 2/2 FINAL) ( #143 )
...
GateVerdictBanner с 4 visual states + expand/collapse details.
Types: GateVerdict/GateBlocker/GateWarning/GateVerdictSource/GateVerdictLabel.
Renders at top of OverviewTab driven by backend aggregator (#142 ).
tsc 0 errors, ESLint 0 warnings.
Vault: code/modules/Module_Gate_Verdict_Banner.md NEW.
Closes #32
Co-authored-by: lekss361 <claudestars@proton.me>
2026-05-15 01:09:34 +03:00
lekss361
b42c9dd8f5
feat(site-finder): weight profile panel + analyze options ( #114 sub-PR 4/4 FINAL) ( #139 )
...
Closing FINAL sub-PR for #114 Custom POI weights — UI complete.
- weightProfiles.ts: TS types + 4 TanStack Query hooks + 11 categories
- WeightProfilePanel.tsx: collapsible panel, sliders, save dialog
- useSiteAnalysis.ts: accepts AnalyzeOptions, query string for backend
- page.tsx: lifts weights state, integrates panel
Approach: 'save first' для ephemeral (no temp profile creation).
tsc 0 errors, ESLint 0 warnings.
Vault: Module_Weight_Profiles_Frontend.md NEW.
Closes #114
Co-authored-by: lekss361 <claudestars@proton.me>
2026-05-15 00:50:02 +03:00
lekss361
2416ed0db1
refactor(frontend): extract SectionLabel + EmptyState + adminStyles shared ( #132 )
...
* fix(docs): block javascript: URLs in renderMarkdown href (XSS guard)
Per audit batch #127 P2 security minor (issue #130 ).
## Risk
`renderMarkdown` substituted `[text](url)` → `<a href="...url...">` verbatim
без URL validation. Edge case: `[click](javascript:alert(1))` →
`<a href="javascript:alert(1)">click</a>` execute'нется при click.
Currently low risk (markdown source = build-time fs.readFileSync из
public/docs/, checked into repo), но защита-в-глубину: если когда-либо
markdown source станет user-supplied, vuln materialize.
## Fix
`frontend/src/app/docs/b2b-channels/renderMarkdown.ts`:
1. New `safeUrl(url)` function — allowlist `https://`, `http://`, `/`, `#`,
`mailto:`. Everything else → `"#"`.
2. Link regex replacement switched from backreference string to callback
so URL passes через `safeUrl` ДО injection в href.
3. URL unescape → safeUrl → re-escape (HTML escape applied per-line).
## Test cases verified
- `[ok](https://example.com )` → href=`https://example.com ` ✅
- `[ok](/local)` → href=`/local` ✅
- `[ok](#anchor)` → href=`#anchor` ✅
- `[ok](mailto:foo@bar.com)` → href=`mailto:foo@bar.com` ✅
- `[bad](javascript:alert(1))` → href=`#` ✅
- `[bad](data:text/html,...)` → href=`#` ✅
- `[bad](vbscript:msgbox(1))` → href=`#` ✅
## Checks
- tsc: 0 errors
- lint: 0 warnings
- No existing `__tests__/renderMarkdown` to break
## Vault
`fixes/Bug_RenderMarkdown_JavascriptUrl_May14.md` — created.
Closes #130
* refactor(frontend): SectionLabel + EmptyState + adminStyles shared (fixup)
Per audit batch #127 P2 hygiene (issue #129 ). Previous commit лошил
файлы из-за pre-commit (видимо). Fixup-коммит с actual diff:
## New shared modules (3)
- frontend/src/components/ui/SectionLabel.tsx
- frontend/src/components/ui/EmptyState.tsx
- frontend/src/lib/adminStyles.ts
## Replacements
- SectionLabel: 12 inline usages → import (Overview/Land/Market/Environment Tab)
- EmptyState: 3 inline usages → import (Land/Market/Environment)
- adminStyles: 5 admin pages import cardStyle/labelStyle/inputStyle/th/td
- BulkGeoPanel: cardStyle only (preserves local labelStyle для span)
- leads/page.tsx: cardStyle с marginTop extend; th/td/inputStyle local (divergent)
## Visual regression: ZERO
All px/colors/CSS properties copied verbatim. Files с divergent values left local.
## Checks
- ruff (no files)
- prettier
- tsc + lint clean (per agent run)
Closes #129
Refs: #127
* docs(adminStyles): JSDoc warning про divergent local overrides
Per bot review #132 non-blocking minor — задокументировать почему
leads/page.tsx и BulkGeoPanel.tsx имеют local overrides, чтобы будущий
читатель не «унифицировал» обратно по ошибке.
Refs: #129 , #132
---------
Co-authored-by: lekss361 <claudestars@proton.me>
2026-05-14 23:55:38 +03:00
lekss361
8647114b22
fix(docs): block javascript: URLs in renderMarkdown href (XSS guard) ( #131 )
...
Per audit batch #127 P2 security minor (issue #130 ).
## Risk
`renderMarkdown` substituted `[text](url)` → `<a href="...url...">` verbatim
без URL validation. Edge case: `[click](javascript:alert(1))` →
`<a href="javascript:alert(1)">click</a>` execute'нется при click.
Currently low risk (markdown source = build-time fs.readFileSync из
public/docs/, checked into repo), но защита-в-глубину: если когда-либо
markdown source станет user-supplied, vuln materialize.
## Fix
`frontend/src/app/docs/b2b-channels/renderMarkdown.ts`:
1. New `safeUrl(url)` function — allowlist `https://`, `http://`, `/`, `#`,
`mailto:`. Everything else → `"#"`.
2. Link regex replacement switched from backreference string to callback
so URL passes через `safeUrl` ДО injection в href.
3. URL unescape → safeUrl → re-escape (HTML escape applied per-line).
## Test cases verified
- `[ok](https://example.com )` → href=`https://example.com ` ✅
- `[ok](/local)` → href=`/local` ✅
- `[ok](#anchor)` → href=`#anchor` ✅
- `[ok](mailto:foo@bar.com)` → href=`mailto:foo@bar.com` ✅
- `[bad](javascript:alert(1))` → href=`#` ✅
- `[bad](data:text/html,...)` → href=`#` ✅
- `[bad](vbscript:msgbox(1))` → href=`#` ✅
## Checks
- tsc: 0 errors
- lint: 0 warnings
- No existing `__tests__/renderMarkdown` to break
## Vault
`fixes/Bug_RenderMarkdown_JavascriptUrl_May14.md` — created.
Closes #130
Co-authored-by: lekss361 <claudestars@proton.me>
2026-05-14 23:39:47 +03:00