fix(sf-fe): /site-finder — adapt B1 response shape (markers invisible) + restore CadInput #349

Merged
lekss361 merged 1 commit from fix/sf-entry-parcel-adapter-cadinput into main 2026-05-17 23:20:58 +00:00
Owner

Summary

2 prod регрессии замеченных user после deploy #347:

«не видно участков · и нельзя просто ввести»

Bug A — markers невидимы

Backend (B1) возвращает:

{ "parcels": [{"cad_num":"...","centroid_lat":56.82,"centroid_lon":60.68,"area_m2":2945931,"status":null,"land_category":null}], "count":5, "limit":5, "bbox_area_km2":100 }

Frontend useParcelsBboxQuery типизировал ответ как плоский ParcelBboxItem[] с полями {lat, lon, area_ha, district, vri, address, status}. Результат:

  1. .map() итерировался по объекту, а не массиву → CircleMarker'ы не создавались
  2. Даже если бы — center=[undefined, undefined] из-за разницы имён

Fix: adapter в queryFn распаковывает .parcels, переименовывает centroid_lat/centroid_lon → lat/lon, конвертирует area_m2 → area_ha (÷10000), status: null → 'free' по умолчанию. district/vri/address — placeholders до enrichment B1 ( / other / ""). applyFilters остаётся client-side.

Bug B — нет ручного ввода кадастра

A2 #343 при замене placeholders «потерял» существующий <CadInput/> компонент (вход для quick analysis без клика по карте). Восстановлен в правый sidebar над <RecentParcels/>. Submit → router.push('/site-finder/analysis/{cad}').

Verify

После deploy:

  • /site-finder — CircleMarker'ы на карте, tooltip с XX га
  • Sidebar справа: CadInput с placeholder 66:41:0204016:10 + кнопка «Анализировать»
  • Ввод валидного cad → navigation на /site-finder/analysis/{cad}

Mock-mode (NEXT_PUBLIC_USE_MOCKS=true) не affected — фикстура ParcelBboxItem уже в правильном shape.

Tracking: B1 backend extension (district / vri / address) — vault code/modules/site-finder/B1-enrichment.md (TBD).

## Summary 2 prod регрессии замеченных user после deploy #347: > «не видно участков · и нельзя просто ввести» ### Bug A — markers невидимы Backend (B1) возвращает: ```json { "parcels": [{"cad_num":"...","centroid_lat":56.82,"centroid_lon":60.68,"area_m2":2945931,"status":null,"land_category":null}], "count":5, "limit":5, "bbox_area_km2":100 } ``` Frontend `useParcelsBboxQuery` типизировал ответ как **плоский** `ParcelBboxItem[]` с полями `{lat, lon, area_ha, district, vri, address, status}`. Результат: 1. `.map()` итерировался по объекту, а не массиву → CircleMarker'ы не создавались 2. Даже если бы — `center=[undefined, undefined]` из-за разницы имён **Fix**: adapter в `queryFn` распаковывает `.parcels`, переименовывает `centroid_lat/centroid_lon → lat/lon`, конвертирует `area_m2 → area_ha` (÷10000), `status: null → 'free'` по умолчанию. `district/vri/address` — placeholders до enrichment B1 (`—` / `other` / `""`). `applyFilters` остаётся client-side. ### Bug B — нет ручного ввода кадастра A2 #343 при замене placeholders «потерял» существующий `<CadInput/>` компонент (вход для quick analysis без клика по карте). Восстановлен в правый sidebar над `<RecentParcels/>`. Submit → `router.push('/site-finder/analysis/{cad}')`. ## Verify После deploy: - [ ] `/site-finder` — CircleMarker'ы на карте, tooltip с `XX га` - [ ] Sidebar справа: CadInput с placeholder `66:41:0204016:10` + кнопка «Анализировать» - [ ] Ввод валидного cad → navigation на `/site-finder/analysis/{cad}` Mock-mode (`NEXT_PUBLIC_USE_MOCKS=true`) не affected — фикстура ParcelBboxItem уже в правильном shape. Tracking: B1 backend extension (district / vri / address) — vault `code/modules/site-finder/B1-enrichment.md` (TBD).
lekss361 added 1 commit 2026-05-17 23:17:45 +00:00
Two prod regressions noticed by user after #347 deploy ('не видно
участков · и нельзя просто ввести'):

1. Markers invisible — backend B1 returns
   { parcels: [{cad_num, centroid_lat, centroid_lon, area_m2, status, ...}], count, limit, bbox_area_km2 }
   but useParcelsBboxQuery typed the response as a flat ParcelBboxItem[]
   with fields { lat, lon, area_ha, district, vri, address, status }.
   Result: .map() iterated over an object (no markers rendered) and
   even if iterated, every CircleMarker got center=[undefined, undefined].
   Added an adapter in queryFn: unwrap .parcels, rename
   centroid_lat/centroid_lon -> lat/lon, convert area_m2 -> area_ha,
   default status to 'free' when null, placeholder district/vri/address
   until B1 enrichment lands. applyFilters still runs client-side.

2. No manual cad entry — A2 #343 dropped the existing CadInput component
   from the entry page when replacing placeholders. Re-wired it in the
   right sidebar above RecentParcels; submit -> router.push to
   /site-finder/analysis/{cad} (matches A1 routing scheme).
Author
Owner

Deep Code Review — verdict APPROVE

Status: APPROVE
Files reviewed: 2 (P2: frontend/src/app/site-finder/page.tsx, frontend/src/lib/site-finder-api.ts)
Lines: +35 / -3
PR: #349

Adapter correctness (site-finder-api.ts)

  • raw.parcels.map(...) correctly unpacks {parcels, count, limit, bbox_area_km2} to ParcelBboxItem[] — fixes the root cause where .map() ran on object instead of array.
  • Field rename centroid_lat/centroid_lon -> lat/lon, area_m2 / 10000 -> area_ha — correct (1 ha = 10 000 m²). Null-safety on area_m2 != null ? .../10000 : 0 is right.
  • status ?? "free" is type-safe — ParcelStatus = "free" | "in_progress" | "favorite" so "free" is a valid literal default.
  • Placeholders district: "—", vri: "other", address: "" are safe in context:
    • applyFilters special-cases vri === "other" (skip filter) and district === "" (skip filter), so these placeholders won't accidentally filter out all rows when a user picks a real filter.
    • Minor follow-up: district: "—" means a user-selected district !== "—" will reject everything; in practice the district dropdown is populated from real values so it's benign. Will resolve naturally when B1 enrichment lands.
  • applyFilters reused client-side — matches existing mock-path semantics, consistent.
  • Mock path (MOCK_PARCELS_BBOX) untouched, still returns ParcelBboxItem[] directly from fixture — correct.

CadInput restore (page.tsx)

  • Component signature (cadNum: string) => void, loading: boolean matches <CadInput onSubmit={handleCadSubmit} loading={false} /> exactly.
  • CadInput has internal CAD_REGEX = /^\d+:\d+:\d+(?::\d+)?$/ pre-validation before calling onSubmit, so the value reaching encodeURIComponent(cad) is already constrained to digits + colons → URL-safe.
  • router.push(...encodeURIComponent(cad)) — correct Next.js navigation, no full reload, no XSS (validated input + percent-encoding).
  • Placement above <RecentParcels/> in the right sidebar matches PR description.

TS strict / conventions

  • No any, inline type for raw is explicit and bounded.
  • TanStack Query pattern preserved (queryKey unchanged, staleTime unchanged → no cache invalidation needed).
  • No new XSS surface (cad regex + encodeURIComponent).

Blast radius

  • useParcelsBboxQuery callers (page.tsx, map components) continue to receive ParcelBboxItem[] — public hook signature unchanged, no callers need updating.
  • No backend changes, no migration, no codegen needed.

Merging via squash.

## Deep Code Review — verdict APPROVE **Status**: APPROVE **Files reviewed**: 2 (P2: `frontend/src/app/site-finder/page.tsx`, `frontend/src/lib/site-finder-api.ts`) **Lines**: +35 / -3 **PR**: #349 ### Adapter correctness (site-finder-api.ts) - `raw.parcels.map(...)` correctly unpacks `{parcels, count, limit, bbox_area_km2}` to `ParcelBboxItem[]` — fixes the root cause where `.map()` ran on object instead of array. - Field rename `centroid_lat/centroid_lon -> lat/lon`, `area_m2 / 10000 -> area_ha` — correct (1 ha = 10 000 m²). Null-safety on `area_m2 != null ? .../10000 : 0` is right. - `status ?? "free"` is type-safe — `ParcelStatus = "free" | "in_progress" | "favorite"` so `"free"` is a valid literal default. - Placeholders `district: "—"`, `vri: "other"`, `address: ""` are safe in context: - `applyFilters` special-cases `vri === "other"` (skip filter) and `district === ""` (skip filter), so these placeholders won't accidentally filter out all rows when a user picks a real filter. - Minor follow-up: `district: "—"` means a user-selected `district !== "—"` will reject everything; in practice the district dropdown is populated from real values so it's benign. Will resolve naturally when B1 enrichment lands. - `applyFilters` reused client-side — matches existing mock-path semantics, consistent. - Mock path (`MOCK_PARCELS_BBOX`) untouched, still returns `ParcelBboxItem[]` directly from fixture — correct. ### CadInput restore (page.tsx) - Component signature `(cadNum: string) => void, loading: boolean` matches `<CadInput onSubmit={handleCadSubmit} loading={false} />` exactly. - `CadInput` has internal `CAD_REGEX = /^\d+:\d+:\d+(?::\d+)?$/` pre-validation before calling `onSubmit`, so the value reaching `encodeURIComponent(cad)` is already constrained to digits + colons → URL-safe. - `router.push(...encodeURIComponent(cad))` — correct Next.js navigation, no full reload, no XSS (validated input + percent-encoding). - Placement above `<RecentParcels/>` in the right sidebar matches PR description. ### TS strict / conventions - No `any`, inline type for `raw` is explicit and bounded. - TanStack Query pattern preserved (`queryKey` unchanged, `staleTime` unchanged → no cache invalidation needed). - No new XSS surface (cad regex + encodeURIComponent). ### Blast radius - `useParcelsBboxQuery` callers (`page.tsx`, map components) continue to receive `ParcelBboxItem[]` — public hook signature unchanged, no callers need updating. - No backend changes, no migration, no codegen needed. Merging via squash.
lekss361 merged commit f87a352292 into main 2026-05-17 23:20:58 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#349
No description provided.