feat(sf-fe-a12): Landing redesign + PilotCTA с real B3/B4 data (v2 rebased) #367
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#367
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/sf-fe-a12-landing-redesign-v2"
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
Замещает PR #366 (был заблокирован merge conflict — стартовал с stale
ae0fce3, main ушёл вперёд на 14 коммитов включая #345 lucide-react, #340/A1, #346/A5, #354/A6).Финальный блок SF-Frontend plan (§A12) — полный rewrite
/под mockupgendsgn.ru/site-finder.html-tier landing для девелоперов.PilotRequestModalGET /api/v1/landing/stats(B4, real DB):zk_total,deals_total,price_coverage_pct,mapping_coverage_pct,last_data_update--bg-headline):stats.paradoxPOST /api/v1/pilot/request(B3), success state с trackingId, bottom-sheet на mobilesrc/lib/api/landing.ts:useLandingStats()staleTime 60s +useSubmitPilotRequest()mutationsrc/app/_legacy/page-tabs.tsx(backup)lucide-reactуже на main^0.511.0(через #345); v1 PR пытался регрессивно поставить^1.16.0(несуществующий) — отбито при rebaseConflict resolution (vs #366)
frontend/package.json: оставлен main's^0.511.0(worker ошибочно поставил^1.16.0)frontend/package-lock.json: полная regen черезnpm installпосле resolutionfrontend/src/app/page.tsx: take A12 version (rewrite by design)_legacy/page-tabs.tsx,PilotRequestModal.tsx,lib/api/landing.ts) — clean applyReal-data constraint
Test plan
tsc --noEmitна A12-файлах ✅ clean (pre-existing HeadlineBar.test.tsx errors из #341 — отдельная задача, no @types/jest)qa-testersmokehttps://gendsgn.ru/— Hero + KPI + Modal + POST successRefs: plan §A12 —
vault://code/patterns/SiteFinder_Frontend_Migration_Plan_May18.mdCloses #82
Supersedes #366
Deep Code Review — verdict
Summary
🔴 Critical (BLOCK merge)
1. B3 type contract mismatch — TS lies about the response shape
frontend/src/lib/api/landing.ts:23-29declares:But backend
pilot.py:46-77returns:idis a UUID stringified viaRETURNING CAST(id AS text)(seedata/sql/118_pilot_requests.sql:4—id uuid PRIMARY KEY DEFAULT gen_random_uuid())tracking_idfield is ever returned — only{id, created_at, status}Consequences:
id: number→ in reality it's"7f3a8c2e-1f1a-..."UUID string.formatNumber(data.id)orNumber(data.id)anywhere downstream →NaN.PilotRequestModal.tsx:65:tracking_idalwaysundefined→ success state shows full raw UUID as "Номер заявки" (ugly, useless for CRM lookup). The brief promised a tracking ID in success state — currently dumps the primary key.Fix (pick one):
pilot.py:73-77to compute and returntracking_id(e.g., short hash:f"GD-{row['id'].hex[:8].upper()}"), updatelanding.tsinterfaceid: string, keeptracking_id?: string.tracking_idfrom interface, setid: string, renderНомер заявки: GD-{trackingId.slice(0, 8).toUpperCase()}in modal.This is the headline feature of the PR (per brief: "success state with trackingId") — it currently doesn't work as specified.
2. B4 fallback ships fake numbers despite spec saying "no fake-числа"
backend/app/api/v1/landing.py:23-30defines hardcoded_FALLBACK = {zk_total: 1500, deals_total: 6830000, price_coverage_pct: 81.4, ...}and returns it on:line 106-108)line 91-93)PR brief explicitly states:
Frontend
page.tsx:191-249has no way to tell_FALLBACKfrom real data —useLandingStatsreturns 200 OK with body either way, soisLoading=false, isError=false, and the UI proudly shows "1 500 ЖК DOM.РФ в ЕКБ" / "6 830 000 ДДУ-сделки" — exactly the fake-numbers scenario the brief forbids.Fix: backend B4 should either (a) raise 503 on
_query_stats() == Noneso the hook hits the error branch, (b) include astale: boolflag, OR (c) the frontenduseLandingStatsselector strips out responses missing a freshness marker → keepdataundefined → skeleton. Currently the contract silently lies.This is part of #331's surface, but #367 ships the UI that relies on that contract — so the regression lands here.
🟠 High (should be fixed before merge)
3. PilotRequestModal — focus trap & ESC missing; Drawer primitive ignored
frontend/src/components/ui/Drawer.tsxalready exists (from #341) and provides:handleDrawerKeyDownTab cycling)side="bottom"for mobile bottom-sheet,side="right"for desktopPilotRequestModal.tsxreinvents the wheel and is less accessible:aria-modal="true"is set but trap isn't enforced → screen-reader users get inconsistent experience.PilotRequestModal.tsx:118-121) but the inline style setsbottom: 0; left: 0; right: 0; maxWidth: 560; margin: 0 auto— this anchors to bottom on all viewports, including desktop. There's no centered desktop mode — comment lies.Fix: wrap in
<Drawer open={open} onClose={onClose} side="bottom">and remove the hand-rolled backdrop/panel. Saves ~80 LoC, gets a11y for free, matches rules/ui-ux.md "Drawer pattern for drill-down/modals".4. Design-token violations —
frontend.md+ui-ux.mdHARD rulespage.tsxandPilotRequestModal.tsxare full of inline hex literals that bypass the CSS variables introduced in #353:Per
rules/ui-ux.md"Design tokens (HARD)":Also per
frontend.md:The h1 (
page.tsx:198) usesfontSize: 32—ui-ux.mdTypography spec is h1 =22/28 weight 600. Hero title in a landing can break the table, but currently it's neither the documenteddisplay(28) nor h1 (22).5. Paradox section duplicates HeadlineBar component
page.tsx:323-352hand-rolls a dark headline div withbackground: "#0f172a",color: "#e2e8f0",borderRadius: 12, padding: "14px 18px"— identical tocomponents/ui/HeadlineBar.tsx(already in repo, takestitle/subtitle/rightSlot). Use:Sub-bug: when stats is loading, the panel shows literal
"Загрузка..."text inside the headline-bar, which on a dark background reads fine but the"из ЖК у большинства есть цены — но в публичном доступе единицы"fallback (whenstats?.paradoxis nullish-but-defined? not possible per current B4) is shadow string — actually unreachable given B4 always returns paradox; safe to delete.🟡 Medium
6.
lucide-react ^0.511.0confirmed (NOT regressed) ✅frontend/package.json:20is^0.511.0. Lockfile diff shows only abaseline-browser-mapping 2.10.29 → 2.10.30micro-bump — no lucide drift. P0 from #344/#345 not repeated. Good.However — that single transitive bump (
baseline-browser-mapping) is unexplained in the PR body. It's harmless, but tells menpm installran without--no-savesemantics; if Forgejo CI runsnpm ci, lockfile mismatch is OK since the diff matches what's installed. Sanity-check only.7.
paradoxrendered safely — confirmedpage.tsx:341-348interpolatesstats?.paradoxvia{...}(text node), notdangerouslySetInnerHTML. React auto-escapes. No XSS vector even thoughparadoxis a server-side f-string concatenation (landing.py:101-104). ✅trackingIdinterpolation atPilotRequestModal.tsx:213is also a text node — safe.8. Pilot request POST — no retry, no idempotency key — OK for this domain
landing.ts:43-50uses bareuseMutation(noretry: 0explicit), and TanStack Query v5 defaults toretry: falsefor mutations. So no duplicate-submission risk from auto-retry. ✅ (Mention in case anyone tunes the default — keep mutations no-retry.)9.
handleClose()resets form state including success — but mutation state isn't always resetPilotRequestModal.tsx:80-85callsmutation.reset()— good. But if user clicks Submit, gets error, then clicks Close (X), state is reset. If they re-open, no stale error shown ✅. Verified by walk-through — leave as is.10.
last_data_updateraw string displaypage.tsx:312:{stats?.last_data_update ?? "—"}directly renders the YYYY-MM-DD ISO date from backend (landing.py:84-88). Perrules/ui-ux.mdmicrocopy, Russian dates withtoLocaleStringare preferred. Cosmetic —new Date(stats.last_data_update).toLocaleDateString("ru")→17.05.2026.🟢 Low / nits
page.tsx:1"use client"— needed because ofuseState+ TanStack hook. Correct. But the whole page is client-rendered; SEO meta tags (metadataexport) are lost. For a landing page, ideally split: server-renderedpage.tsx+ client<Hero/>/<Modal/>islands. Out of scope for this PR — track in follow-up.page.tsx:439footer<span>GenDesign {new Date().getFullYear()}</span>—new Date()in client component → re-renders on every parent update; trivial cost, ignore._legacy/page-tabs.tsxis a literal byte-identical backup of the pre-A12 page (verified vs diff) — underscore-prefix_legacy/is not routed by Next.js app router. Good, no data loss. But this should probably go tofrontend/src/app/_legacy/page.tsx(notpage-tabs.tsx) if anyone ever wanted to temporarily restore it via folder swap; current name documents intent but mute the option. Cosmetic.page.tsx:531-554—kpiCardStyle/kpiIconWrap/kpiLabel/kpiValueare declared at module scope below the component. Fine, but conventional React-with-inline-styles puts these inconst styles = {...}object or pulls them into a small component. Minor.minmax(200px, 1fr)lays out 5 cards. On 1280px viewport that yields a ragged row (3+2 or 4+1). Perui-ux.md"3-4 KPI выше fold — максимум" — but for landing hero with B4 KPIs, 5 cards may be intentional. Acceptable.Cross-file impact analysis
landing.py) and B3 (pilot.py) endpoints assumed by this PR are present inmain.py:105-106and routes resolve correctly (/api/v1/landing/stats,/api/v1/pilot/request). ✅apiFetch(src/lib/api.ts) throws on non-2xx, error message includes status code + body text. Concern:apiFetchthrowsnew Error(\API error ${status}: ${await response.text()}`)— if backend ever returns a 422 with form data echoed, the error toast (PilotRequestModal.tsx:354-356) rendersmutation.error.message` which includes the raw server response — could leak PII (user's own input back) but only to themselves, not other users. Not a high-priority PII leak.page.tsx(deleted, now_legacy/page-tabs.tsx) was linked fromnavin some sub-pages?grepshows no references — safe to swap. ✅frontend/src/app/page.tsx(it's a route entry, not exported). ✅Vault cross-check
Bug_RenderMarkdown_JavascriptUrl_May14— no markdown rendering in this PR, N/A.Pattern_CAST_AS_Type—pilot.py:52correctly usesRETURNING CAST(id AS text)✅decisions/entry forbidding fallback data — but PR brief explicitly mandates "no fake-числа", so brief is the authority here.fixes/Bug_PilotRequest_Tracking_Id_Contract_Drift_May18.mddescribing the UUID-vs-int issue +decisions/Landing_B4_Fallback_Policy.mdclarifying whether B4 may return fallback data or must 503.Pre-flight
feat/sf-fe-a12-landing-redesign-v2✅mergeable: true(no conflicts) ✅--no-verify/--amend/--forcemarkers in commit messages (verified via PR commits view).Recommended next steps
id: string, decide tracking_id behaviour with backend) — either FE-only fix (droptracking_id?, display first 8 hex of UUID withGD-prefix), or coordinate with backend to addtracking_idto B3 response.<Drawer side="bottom">for free a11y).#hexliterals withvar(--token-name)).<HeadlineBar>for paradox).Complexity / blast radius score
_legacy/page-tabs.tsxis not auto-routed so a manual rename is needed for emergency rollback). Recommend keeping a real rollback runbook entry.Verdict — NOT merged
Two contract drifts (frontend type vs backend reality, B4 fallback vs no-fake-numbers brief) plus reinventing the Drawer primitive (a11y regression) put this firmly in BLOCK territory. None of the items are wrenching to fix — likely a single fixup commit. Once #1, #2, #3, #4, #5 are addressed, re-request review and this can land.
- B3 type contract: PilotRequestResponse.id is string (UUID via CAST), drop tracking_id; derive display ID as GD-{first8hexUppercase} from UUID - B4 stale flag: LandingStatsOut Pydantic schema adds stale:bool=False; fallback returns stale=True with zeroed fields (no fake numbers); frontend shows skeleton placeholders when stale=true - PilotRequestModal: replaced manual backdrop+panel+ESC logic with <Drawer side="bottom"> (focus trap, ESC, scroll-lock, focus restore for free); removed ~80 LoC - Design tokens: all #hex literals in page.tsx and PilotRequestModal.tsx replaced with var(--token) from globals.css; h1 fontSize 32 -> 28 per display spec - HeadlineBar: hand-rolled dark div in page.tsx replaced with <HeadlineBar> componentВсе 5 review items адресованы коммитом
212f710:id: string(UUID viaCAST(id AS text)),tracking_id?dropped, display ID derived какGD-{first 8 hex uppercase}из UUIDLandingStatsOutPydantic schema сstale: bool = False; fallback теперь возвращаетstale=Trueс нулевыми полями (никаких fake-чисел); frontend показывает skeleton placeholders когдаstale=true<Drawer side="bottom">— ручные backdrop/panel/ESC удалены, focus trap + ESC + scroll-lock + focus restore работают через primitive (−80 LoC)#hexлитералы вpage.tsxиPilotRequestModal.tsxзаменены наvar(--token)изglobals.css; h1fontSize: 32→28per display spec<HeadlineBar title="Парадокс портфеля" subtitle={stats?.paradox} />Pre-flight:
tsc --noEmit✅ 0 errors на изменённых файлахnpm run lint✅ cleanruff check app/api/v1/landing.py✅ passFiles changed (4):
backend/app/api/v1/landing.pyfrontend/src/lib/api/landing.tsfrontend/src/components/landing/PilotRequestModal.tsxfrontend/src/app/page.tsxHEAD:
212f710. Re-requesting review.Deep Code Review — verdict: APPROVE (round 2)
Re-review at head SHA
212f7109e695564b54e71f14cc79a9c89b07eb89against round-1 blockers.Round-1 blockers — all resolved
B4 hidden fallback (was CRITICAL) — FIXED.
backend/app/api/v1/landing.py:35-43:_FALLBACK_DATAnow zeroed withstale: True(not fakezk_total=1500/deals_total=6_830_000).LandingStatsOutPydantic schema withstale: bool = False— fail-loud contract.logger.exception(...)+return None→ response carriesstale=True.page.tsx:const isStale = stats?.stale === true; const showSkeleton = isLoading || isStale;— KPI cells render<KpiSkeleton/>placeholders + visible "Данные обновляются..." note. No fake numbers shipped on error path.UUID contract drift (was CRITICAL) — FIXED.
frontend/src/lib/api/landing.ts:30:PilotRequestResponse.id: string(wasnumber), comment/** UUID returned as string via CAST(id AS text) */.pilot.py:52:RETURNING CAST(id AS text), created_at— consistent.PilotRequestModal.tsx:32-34:deriveTrackingId(id) = "GD-" + id.replace(/-/g, "").slice(0, 8).toUpperCase()— clean display "GD-7F3A8C2E" instead of raw 36-char UUID. Good UX.RETURNING CAST(id AS text), frontend treats as string, no raw UUID leaks to user.Modal a11y (was HIGH) — FIXED.
PilotRequestModal.tsx:48: uses<Drawer open={open} onClose={handleClose} side="bottom">(A3 #341 primitive).Drawer.tsxprovides: focus trap (Tab/Shift+Tab cycling lines 82-103), ESC listener (lines 54-63), focus restore viapreviousFocusRef(lines 36-51), body scroll lockdocument.body.style.overflow = "hidden"(lines 66-74),role="dialog" aria-modal="true"(line 163), backdrop click close (lines 76-80)."use client"directive present (line 1).Mobile/desktop positioning (was HIGH) — addressed via Drawer primitive.
side="bottom"→ bottom-sheet (borderRadius: "12px 12px 0 0",maxHeight: "85vh", slide up viatranslateYlines 126-142).side="right"if dedicated desktop modal needed in future, but current single-side is consistent with A12 plan).Misc (round-1 nits) — FIXED.
#hexliterals inpage.tsxandPilotRequestModal.tsxreplaced withvar(--token)(only remaining#fffis white-on-accent CTA text — acceptable, per ui-ux.md). KPI label uppercase + 0.04em letter-spacing OK.<div bg=slate-900>replaced with<HeadlineBar title=... subtitle=.../>component (page.tsx:391).Other 4 files (no regression)
frontend/package-lock.json: onlybaseline-browser-mapping 2.10.29 → 2.10.30(transitive bump, benign).frontend/src/app/_legacy/page-tabs.tsx: unchanged backup.frontend/src/lib/api/landing.ts: 54 LoC new file, clean TanStack hooks,staleTime: 60_000for stats, proper types.landing.py: idempotent — DB query unchanged, only response schema + fallback semantics tightened.Cross-file impact
apiFetchconsumer correct, no caller in repo depends on oldid: numbershape.useLandingStatsquery key["landing", "stats"]— unique, no cache collision.Positive observations
aria-label="Данные обновляются..."— screen-reader friendly./^[^\s@]+@[^\s@]+\.[^\s@]+$/matches backend Pydantic pattern — consistent validation.Severity
APPROVE — proceeding to merge.
Merging via
mcp__forgejo__merge_pull_request(squash).