feat(sf-fe-a5): Section 1 Инфо об участке — HeadlineBar + KPI + EGRN + POI + Export #346
No reviewers
Labels
No labels
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
Fable 5 ревью
feedback/max
generative
GG-форсайт
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
вторичка
ИРД
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#346
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/sf-fe-a5-section1-parcel-info"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Mock toggle
NEXT_PUBLIC_USE_MOCKS=true uses fixtures. Flip MOCK_ANALYZE/MOCK_POI_SCORE=false when B5/B6 deployed.
Constraints
- Section1ParcelInfo: wrapper с HeadlineBar вердикт, 4 KpiCard (Площадь/Район/Медиана/Score), 2-col grid (MiniMap + EgrnPropertyTable + PoiList2Gis), ExportButtons. Anchor id="section-1" для scrollspy. - MiniMap: dynamic import SiteMap (ssr:false), 400×320, адаптер ParcelAnalyzeResponse → ParcelAnalysis. - EgrnPropertyTable: 10 EGRN полей key-value, monospace для cad_num/area_m2, zebra-stripe. - PoiList2Gis: top-7 POI weighted score, Lucide иконки (Train/TreePine/GraduationCap/Baby/ShoppingBag/Hospital), distance + weight Badge, fallback из score_breakdown. - ExportButtons: PDF blob download (B7 /snapshot.pdf) + frontend CSV из analyze response, loading state, error display. - site-finder-api.ts: useParcelAnalyzeQuery (B5) + useParcelPoiScoreQuery (B6) с MOCK_ANALYZE/MOCK_POI_SCORE fallback. - mocks: parcel-analyze.json + poi-score.json fixtures для dev. - page.tsx: Section 1 placeholder заменён на <Section1ParcelInfo cad={cad} />.Deep Code Review — PR #346 (sf-fe-a5 Section 1)
Summary
package.json/ lockfile diff — uses already-installedlucide-react,@tanstack/react-query,next/dynamic,geojson✅🟠 HIGH (block merge)
1. CSV injection —
ExportButtons.tsx:74-80(escapeCsvCell)Current
escapeCsvCellonly quotes when the value contains,,",\n,\r. Values starting with=,+,-,@, or tab/CR are NOT neutralized → Excel/LibreOffice/Google Sheets interpret these as formulas (DDE /=HYPERLINK/=cmd|...etc).User-controlled strings flowing into CSV from
buildCsvRows:egrn.address— scraped from Rosreestr, attacker-controllable in principleegrn.vri,egrn.encumbrance— same provenancedistrict.district_name— derived data, lower riskdata.cad_num— URL param, fully attacker-controllableRecommended fix:
Reference: OWASP "CSV Injection" / CWE-1236. This is the standard mitigation (leading apostrophe is stripped by Excel on display but neutralizes formula evaluation).
🟡 MEDIUM (fix before B5 cutover, not blocking mock-mode merge)
2. EGRN field-name drift between FE type and BE response —
site-finder-api.ts:218-228Backend (
parcels.py:1837-1858) returnsegrnblock with these keys:Frontend
ParcelEgrndeclares:When
MOCK_ANALYZE=falseflips, the mock JSON shape (matches FE type) will diverge from real backend response. Result at runtime:egrn.vri,egrn.category,egrn.owner_type,egrn.encumbrance,egrn.status,egrn.last_updated,egrn.area_m2will beundefined→ table cells render literal "undefined" (not "—") because the fallback inSection1ParcelInfo.tsx:200-204only triggers whendata.egrn == null, not per-field.Two acceptable resolutions:
ParcelEgrnto match backend keys + a small adapter in the hookEither is OK — but pick one before flipping
MOCK_ANALYZE=false. Suggest tracking in vaultcode/modules/site-finder/B5-egrn-contract.md.3. Fallback
area_m2: 0produces "0,00 га" → "0 м²" / "—" mismatch —EgrnPropertyTable.tsx:24-30+Section1ParcelInfo.tsx:222When EGRN absent,
buildFallbackEgrnsetsarea_m2: 0. InEgrnPropertyTablerowПлощадьrenders0 м²(numeric branch always taken), while KPI usesareaHa = egrn.area_m2 > 0 ? ... : "—"so KPI shows "—". Asymmetric. Fix:area_m2: NaNplusNumber.isFinitecheck inbuildRows, or use sentinelnulland update the type.🟢 LOW / nits
Section1ParcelInfo.tsx:53-66—scoreVerdictreturnsПОДХОДИТfor both>=80and>=65branches. Intentional (no separation) but the two-branchiflooks like dead code. Collapse to one branch.Section1ParcelInfo.tsx:275-285— the syntheticPoiList2Gisfallback (when B6 unavailable) hard-codesweight: 0.1, score_contribution: 5— visible "×0.10" badges on every item, semantically misleading. Consider an explicit "estimated" label or omit the badge in fallback mode.PoiList2Gis.tsx:46—weightBadgeVariantreturns"warning"for low weights, but Badge "warning" variant is typically reserved for errors/risks. Using"neutral"for the lowest tier is more accurate.style={{ ... }}is used everywhere (133+ instances across 4 new files)..claude/rules/frontend.mddoesn't strictly forbid this, but Tailwind utility classes are the project convention elsewhere insite-finder/analysis/. Not a blocker, just inconsistent with neighbouring code.MiniMap.tsx:49—(data.source as ParcelAnalysis["source"]) ?? "cad_quarter"— narrowascast bypasses strict-mode validation. Safer:data.source === "cad_building" ? "cad_building" : "cad_quarter".Cross-file impact / blast radius
page.tsxchange is minimal (1 placeholder → component). Sections 2-5 untouched ✅MiniMap.tsx:toSiteMapDatacorrectly satisfies the required ParcelAnalysis fields that SiteMap actually reads (cad_num,geom_geojson,score_breakdown). Optional fields (noise/wind/air_quality/...) defaulted tonullwon't break SiteMap (verified:SiteMaponly accessesdata.cad_num,data.geom_geojson,data.score_breakdown) ✅Section1ParcelInfois a leaf component — no callers exist (intended, just imported by page.tsx) ✅Conventions check
any— ✅ (one narrowascast in MiniMap, noted above)useQueryw/ stablequeryKey,staleTime,retry"use client"only where hooks/state used — ✅ EgrnPropertyTable + PoiList2Gis correctly server componentsdangerouslySetInnerHTML— ✅encodeURIComponenton user-controlled URL paths — ✅ (PDF + analyze + poi-score)target="_blank"2GIS link — N/A (component is named PoiList2Gis but no actual external links in this file; consider adding in follow-up if "2GIS" suffix is meant to deeplink)SSR safety on MiniMap
✅
dynamic(...)withssr: falseand a placeholder of identical 320px height. Loading text "Загрузка карты..." is visible. No layout shift. SiteMap is reused (not duplicated) — single source of truth for Leaflet logic.Recommended next steps
area_m2: 0fallback + Badge variant for low weightsOnce #1 is pushed, happy to re-review and merge. Mock-mode (NEXT_PUBLIC_USE_MOCKS=true) is safe to deploy as-is for stakeholder demos — but the CSV button is reachable in mock-mode too, so the formula-injection fix should land before any deploy.
Fixup
47d0374pushed addressing review:ExportButtons.escapeCsvCellтеперь prefixes cells starting=/+/-/@/\t/\rс leading apostrophe (OWASP CWE-1236). Excel/Calc/Sheets больше не evaluate attacker-controllable strings (egrn.address,cad_num) как formulas.buildFallbackEgrnreturnsarea_m2: NaN; и KPI «Площадь», и EgrnPropertyTable row gate наNumber.isFinite(area_m2) && area_m2 > 0, рендерят—consistently.scoreVerdictcollapsed dead branch (>=80/>=65обе возвращалиПОДХОДИТ).MiniMap.toSiteMapDatanarrowssourceчерез explicit ternary вместо uncheckedascast.Открытые items (deferred):
useParcelAnalyzeQueryпосле deploy B5 на prod, чтобы посмотреть actual shape. Tracking: vaultcode/modules/site-finder/B5-egrn-contract.md(будет создан при следующем касании B5).Re-review когда удобно.
Deep Code Review — round 2 verdict
Status: APPROVE
Head:
47d0374386Files: 9 (+1246/-8)
CSV injection fix — verified
escapeCsvCellatfrontend/src/components/site-finder/analysis/ExportButtons.tsx:71-83:Correct mitigation:
=,+,-,@, TAB (\t), CR (\r)=A1→'=A1(emitted raw, spreadsheets strip the')=A1,B1→'=A1,B1→ quoted as"'=A1,B1"(also correct)Other 8 files
Unchanged from round 1 approved state, no regressions.
Follow-up (non-blocking, defer)
ParcelEgrntype and backend B5 keys (still applies from round 1) — track when B5 extended deploys.Merging.