feat(tradein-ui): mock-disclaimer + progressive enrichment + UX polish #517
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#517
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/tradein-ui-mock-disclaimer-enrichment"
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?
Changes
HeroSummarywhentarget_addresshasСклад|Гараж|Подсобка|Нежилое,prefix orconfidence_explanationstarts withaddress_not_geocodedhouse_type+repair_stateif either is NULL AND confidence != low; POST /estimate re-run with merged patchnullifactual_deals.length === 0(no empty section)нет данных(greyed) when nullFiles
tradein-mvp/frontend/src/app/page.tsx(resubmit handler)tradein-mvp/frontend/src/components/trade-in/HeroSummary.tsx(3 changes)tradein-mvp/frontend/src/components/trade-in/DealsCard.tsx(empty-state hide)Validation
Lint/tsc unable to run in isolated worktree (node_modules missing). Pre-commit hooks passed. Type contract follows existing
EstimateResponse/HouseType/RepairStatefromlib/types.Roadmap
PR 7 of 8 —
[[Decision_TradeIn_DataQuality_8PR_Roadmap]].Manual verify after deploy
Склад,).house_type=null— enrichment form visible.Deep Code Review — verdict: REQUEST CHANGES
Summary
Decision_TradeIn_DataQuality_8PR_Roadmapitems #6 (mock disclaimer) + #11 (progressive enrichment). Scope correct.🟠 High — Tailwind classes used but Tailwind NOT installed in
tradein-mvp/frontendFile:
tradein-mvp/frontend/src/components/trade-in/HeroSummary.tsxTwo new blocks use Tailwind utility classes:
text-sm font-medium,control(custom),btn btn-primary(custom — these work).Problem:
tradein-mvp/frontend/package.jsonhas NOtailwindcssdependency. There is notailwind.config.{js,ts}, nopostcss.config.*, no@tailwinddirective inglobals.cssortrade-in.css. The whole subproject uses semantic class names (card,card-head,meta-row,hero-card, …) backed bytrade-in.csswith OKLCH design tokens.Effect: all Tailwind classes are inert. The mock disclaimer (THE headline feature of this PR — per roadmap #6) will render as plain unstyled text on the card surface — no amber border, no warning-background, no padding (only
margin: 16px 16px 0inline applies). That's a functional UX regression of the feature itself: a non-styled warning is no warning.Fix options:
Option A — inline styles (minimal diff, matches existing patterns in this file):
Option B — add CSS classes to
trade-in.css:.hero-disclaimer,.hero-enrich,.hero-enrich select, etc. — preferred for reuse and theme consistency. The file already defines--accent-2/--accent-2-soft(orange/warn) which is the natural choice for this disclaimer.Apply the same to the enrichment form block. Pick one approach but don't ship Tailwind classes here.
🟡 Medium — Mock disclaimer not announced by screen readers
File:
HeroSummary.tsx~104The disclaimer appears conditionally after async data load. Without
role="status"+aria-live="polite"(orrole="alert"for stronger urgency), AT users won't be notified that the estimate is approximate. Add to the wrapper<div>(see snippet above).🟢 Low —
handleResubmitdeclared beforeresultDataFile:
tradein-mvp/frontend/src/app/page.tsxlines 62-65 referenceresultDatadefined at line 69.Runtime-safe (callback fires after render completes; TDZ won't trip), but ESLint
no-use-before-definemay flag this. PR description acknowledges lint never ran in the isolated worktree. Consider moving thehandleResubmitdeclaration AFTERresultData, or convertresultDatato auseMemo/useStatehoisted above.🟢 Low — Disclaimer wording diverges from roadmap
Roadmap (
Decision_TradeIn_DataQuality_8PR_Roadmaprow #6) specifies «Адрес не распознан — оценка примерная (mock-данные)». PR uses «Адрес распознан неточно — … приближённой». The PR phrasing is softer and arguably better UX, but it drops the explicit «mock-данные» marker. Worth a quick alignment ping with product. Not a blocker.Cross-file impact (verified)
DealsCard.tsx: returningnullwhenactual_deals.length === 0— no caller iterates over the rendered output;page.tsxrenders<DealsCard … />inline, so a null render is harmless. ✅page.tsxhandleResubmit: correctly mergesresultData.input + patchand re-runshandleSubmit. URL replace +freshResultoverwrite afterward — TanStack mutation queues on the same key; double-submit guarded byisResubmittingdisabled state. ✅SourcesProgressconsumes the oldestimateduring resubmit pending — no flash-of-mocked-content becauseresultDataonly switches inonSuccess. ✅HouseType/RepairStatere-exported from@/types/trade-in— matches existing schema. ✅est_days_on_marketfield exists onAggregatedEstimateand may benull— handled. ✅Security / XSS (per spec focus)
<select>whose<option value>exactly matches theHouseType/RepairStateunion — theas HouseTypecast is safe.safeUrlusage needed — no URLs constructed from user input in this PR.Performance
useStatefor two strings — negligible. ✅Tests
Pre-flight checks
--no-verify/--amend/--forcein recent commits ✅765b172on top ofe45ff47) ✅Recommended next steps
HeroSummary.tsxwith inline styles using existing--accent-2*/--surface-2tokens (or add.hero-disclaimer/.hero-enrichtotrade-in.css).role="status" aria-live="polite"to the disclaimer wrapper.handleResubmitbelowresultDatadeclaration inpage.tsx.Complexity / blast radius
Deep code review — PR #517 (fresh independent re-review)
Note on SHA: Prior review targeted commit
765b172. PR HEAD has since advanced to81dd103c("fix(tradein-ui): replace Tailwind with OKLCH tokens + a11y on disclaimer"). This review evaluates the current PR head (cumulative state).Prior findings — all resolved
81dd103ctradein-mvp/frontend(no Tailwind installed) → unstyled disclaimerstyle={{}}using OKLCH design tokens (--accent-2,--accent-2-soft,--fg,--radius,--border,--surface-2,--muted). All tokens verified to exist intradein-mvp/frontend/src/components/trade-in/trade-in.css(lines 1-42). Confirmed no Tailwind:package.jsonhas no tailwindcss;Dockerfileline 2 explicitly states «Без Sentry/Tailwind/Leaflet/echarts».aria-liveon dynamic warningrole="status" aria-live="polite"(HeroSummary.tsx ~line 105)handleResubmitreferencesresultDatabefore its declarationhandleResubmitmoved to line 78 ofpage.tsx, afterresultDatadeclaration (line 75)New finding from fresh review
🟡 Medium — Restored-estimate path: resubmit POSTs invalid
area_m2: 0When the user navigates via
?id=<uuid>(restored estimate),resultData.inputis a stub:Clicking «Пересчитать» in the enrichment form calls
handleResubmit({house_type|repair_state})which spreads this stub into the request body. Backend schema requiresarea_m2: float = Field(gt=10, lt=500)→ POST returns 422.The primary form-driven (fresh) path is unaffected and works correctly — the bug is only on the secondary restored-by-
?id=path.AggregatedEstimatealready carries authoritativearea_m2 / floor / total_floors / rooms / year_built / has_balcony(types/trade-in.ts L70-77), so a one-liner backfill inhandleResubmitresolves it. Not a release-blocker but worth a quick follow-up commit before reliance grows.Suggested patch:
Smaller observations (not blocking)
(needsHouseType || needsRepairState) && estimate.confidence !== "low"hides enrichment when confidence islow. Counter-intuitive at first glance (you'd think low confidence is exactly when you'd want enrichment offered), but plausibly intentional: low confidence often means address resolution failed, where enrichment won't help. Worth confirming design intent.enrichHouseType as HouseTypeis guarded by truthy check; state is typedstring. Would be cleaner asuseState<HouseType | "">(""), but current form is safe.isMockAddressheuristic — hardcoded regex list of "Склад/Гараж/Подсобка/Нежилое" +confidence_explanation.startsWith("address_not_geocoded")is acceptable as a frontend heuristic for MVP. Long-term: backend should return an explicittarget_address_is_fallback: boolflag.nullon empty deals is intentional UX cleanup; section disappears entirely. Confirmed no regression:SourcesProgressandHeroSummary's actual_deals price bar both gate independently. ✅Verdict
Approve. All three prior blocking issues are correctly addressed in
81dd103c. The restored-path 422 issue is a real bug but on a secondary code path that doesn't block the primary feature flow; recommend a follow-up commit.