- listings.phones: миграция 166 обнуляет данные; cian.py + scraper-kit
serp.py (golden-parity зеркало) перестают читать offer.get("phones") на
входе — write-path выключен у источника, колонка/поле в модели остаются.
Мёртвая запись "phones" в LISTING_FIELD_PRIORITY убрана.
- trade_in_estimates.client_name/client_phone: миграция 167 дропает колонки;
синхронно убраны из TradeInEstimateInput, обоих INSERT в estimator.py
(основной путь + _empty_estimate), frontend TradeInEstimateInput и
CRM-блока EstimateForm (поля "Имя клиента"/"Телефон").
- sentry_scrub.py и management_companies.phones не тронуты — вне scope.
/me теперь отдаёт display_name/org/email (kopylov -> "Копылов", остальные None).
TopNav использует их вместо фабрикации username-as-name; org/email фолбэк
остаётся прежним для юзеров без известного профиля.
Audit of /trade-in/v2 found the FE-3/FE-4 overlay wiring (mapAnalytics/mapSources
feeding AnalyticsView/SourcesView from useEstimateHouseAnalytics,
useEstimateSellTimeSensitivity, analogs[]/actual_deals[]) already shipped in
267585e3. The remaining gap was BE-1 (#2043, merged 2026-07-02) landing real
cv/source_counts/created_at on AggregatedEstimate AFTER that wiring pass, so
v2/mappers.ts was still FE-approximating cv and duplicating a hardcoded source
label map instead of the single source-registry (#2211).
- mapSources' marketAds.kpi.cv (#2041) now prefers the real estimate.cv
(0..1 ratio from BE-1) and only falls back to the FE coefficient-of-variation
of analog prices for pre-#2043 cached estimates where cv is absent.
- adRows/dealRows source badges (#2041) now resolve via the shared
src/lib/source-registry sourceLabel() instead of a local SOURCE_LABEL map,
picking up etagi/canonical labels and staying in sync with the #2211 roster.
- AnalyticsView (#2040) KPIs/sell-time/price-history/scatterDetail were already
wired to real data with no gaps found against the issue spec.
Хинт диапазона expected_sold дополнен замеренной точностью: «фактическая
цена продажи попадает в диапазон в ~80% случаев (июль 2026)» — по живому
бэктесту на реальных ДКП Росреестра (n=276, coverage 81.5%, engine=full).
Раскрытие вместо молчаливой выдачи Q1-Q3 за прогнозный интервал —
acceptance-пункт «явно раскрыть точность/покрытие» из #2209.
Refs #2209
Three UX fixes on the /trade-in/v2 overlays (feedback):
1. TopNav stays interactive while a section overlay is open. The a11y
audit #2081 put inert on the whole content wrap (which contains
TopNav), so opening an overlay disabled the top tabs. Move inert off
the wrap onto <main>/<footer> only; the nav is inert just for the
full-screen LocationDrawer. Users can now switch sections directly
from an open overlay. role=dialog/aria-modal/Esc/focus-trap kept.
2. «← К ОЦЕНКЕ» made a clear filled primary button (solid accentDeep bg,
white text ≥4.9:1 AA, shadow, hover lift) instead of the weak ghost
pill. Uses existing accent tokens only.
3. Backdrop click closes the overlay. Add a transparent sibling layer
below the panel (z below it, starting under the nav so tabs stay
clickable); clicks in the margin close it. Clicks inside the panel /
on its buttons never reach it (sibling, not ancestor) so they don't
close. Esc + focus restore preserved.
Оценщики клиента должны попадать на новую витрину /trade-in/v2 (wired на
real data), а не на легаси-корень (app/page.tsx, HeroSummary). Добавляю
декларативный redirects() в next.config: source "/" -> destination "/v2".
- permanent:false -> 307 (обратимо, не кэшируется навсегда)
- basePath авто-префиксит -> редирект /trade-in -> /trade-in/v2
- query-string Next пробрасывает автоматически (destination без своего
query) -> /trade-in/?id=<uuid> -> /trade-in/v2?id=<uuid>, restore
оценки по ?id= не ломается (v2 читает ?id= через window.location.search)
- легаси page.tsx / компоненты остаются в репо (только редирект, обратимо)
Оценщик жаловался на «большой интервал» между рекомендованной ценой и
оценкой. Бэктест: ширину коридора косметически не сузить (реальная рыночная
дисперсия + asking→sold gap). Чиним подачу, не число (математика не тронута).
Витрина /trade-in/v2 (активная, wired на real data через mapResultPanel):
- deltaLabel «К РЫНКУ» → «К ОБЪЯВЛ.» (mappers.ts + fixtures.ts): −N% —
разрыв «объявление → ожидаемая сделка», а не «скидка от нас».
- ResultPanel: честная подпись под ценовыми карточками — «Диапазоны
показывают разброс цен на рынке, а не погрешность оценки». Широкий
диапазон не читается как неуверенность расчёта.
- Иерархия expected_sold=headline / median=baseline / ДКП=справочно уже
была (audit H2, #2062/#2081) — не трогаем.
Легаси-витрина / (HeroSummary), тот же принцип и устранение конфликта термина:
- hero-duo: expected_sold — цифра-герой («Оценка · ожидаемая цена продажи»,
34px, accent), median — вторичный «Рекомендованная цена в объявлении»
(20px, приглушённый) вместо двух равнозначных 28px-блоков «Выставить»/
«Получить». Новый модификатор .hero-duo--sold-primary.
- Диапазон sold подписан «рыночный разброс» + honest hint «Диапазон отражает
разброс цен по рынку, а не погрешность оценки».
- Бейдж «−N%» → «−N% к объявлению» + title (разрыв asking↔сделка, не скидка).
- OfferCard: «Оценка по рынку» → «Цена в объявлении» (median ≠ «оценка»;
слово «оценка» на витрине = expected_sold).
Проверка: tsc --noEmit чисто; next build чисто (lint+types, 13 роутов).
По фидбэку: куцый inline-скаттер в 02 РЕЗУЛЬТАТ мало что говорил и дублировал
полный price×time график в Аналитике. Удалён; ряд диапазонов теперь grid 1fr 1fr —
два range-бара (объявления/сделки) на всю ширину, читаются прямее. Полный
точечный график остаётся в оверлее Аналитики (таб 06).
- ResultPanel.tsx: удалён scatter-блок (title + SVG + легенда + «Подробнее»),
grid ряда «1fr 168px 1fr» -> «1fr 1fr»; убраны ставшие мёртвыми токены
scatterDeal / line3.
- data.scatterMini в контракте оставлен (маппер всё ещё считает, просто не
потребляется) — чистку контракта можно сделать отдельно.
tsc + next build green.
#3: переключатель окна метрики (now/45д) — меняет показываемый %
(sale_share_pct↔sale_share_pct_45d), счётчик (active_secondary↔listings_45d) и
сортировку (share_desc↔share_45d_desc, active_desc↔count_45d_desc). 45д-данные
уже были в API — фронт их не показывал (потому тумблер и не находился).
#2: поле «Мин. объявлений в доме» — client-side фильтр по оконному счётчику.
Frontend-only (бэк/API/deps не тронуты). typecheck clean.
NB: маркеры карты пока по now-метрике (мелкий follow-up).
Поиск домов (доля квартир в продаже) вынесен в отдельный продукт, отвязан от
бренда «Мера» (Мера = оценка вторички). Только UI + 1 строка Caddy, бэк не тронут.
- app/sale-share/layout.tsx: своя metadata (title «Поиск домов — gendsgn»).
- components/trade-in/SaleShareHeader.tsx: своя шапка — продукт «Поиск домов»,
без MeraMark и Mera-табов; UserMenu (auth) сохранён.
- app/sale-share/page.tsx: Topbar → SaleShareHeader; убраны «Мера» из
breadcrumb/footer. Функционал/API (/trade-in/api/...) не тронут.
- Caddyfile: gendsgn.ru/sale-share → redir /trade-in/sale-share (короткий адрес;
basePath=/trade-in → true vanity-URL требует отдельного app).
C1 (tokens): darken muted scale to WCAG AA (>=4.5:1 vs worst-case canvas
#e6eef7); reclassify primary labels muted->body2 across components.
H1 (mappers): planning chip derived from rooms actually present in the
analog set («Все планировки» when heterogeneous) — no number shifted.
H2 (ResultPanel): make «ОЖИДАЕМАЯ ЦЕНА СДЕЛКИ» the headline (accent
border/tint/glow + larger value); demote ДКП·Росреестр to a muted
СПРАВОЧНО reference tile. All three values + mapResultPanel wiring intact.
H3 (mappers): parseAddress keeps the house number for prefix-less
addresses («Белинского, 86»); prefixed path unchanged.
M4 (page/HeroBar/Footer): gate dead controls (КАК РАССЧИТАНО / PDF /
ДЕЙСТВИТЕЛЕН ДО / footer validity) on hasEstimate (mounted && estimate && !insufficient).
M5 (LocationDrawer): real dialog — role=dialog/aria-modal/aria-labelledby
+ focus-trap + <button> close + Esc preventDefault/stopPropagation.
M6 (ParamsPanel): address autocomplete -> combobox ARIA + Arrow/Home/End/
Enter/Escape keyboard nav (mirrors Dd listbox).
M7 (Sources/Cache/Analytics/History): ARIA table semantics over the grids;
Analytics scroll region keyboard-focusable (clears the serious
scrollable-region-focusable axe violation); chart SVGs get role=img+title+aria-label.
M8 (CacheView): normalize the ВРЕМЯ column (normTime) to consistent minute precision.
M9 (LocationDrawer): honest «как считаем» source list (Циан, Я.Недвижимость,
Авито, N1.ru + Росреестр) — drop dead Домклик/Restate per #1968.
M10 (ParamsPanel/mapMarkers): subject pin shows the subject's own area; drop
analog pins overlapping the subject center (was misread as subject data).
M11 (ObjectSummary/SourcesView): label «медиана объявлений»; hide all-«—» columns/rows.
M13 (page): clamp artboard scale >=0.88 so micro-type doesn't drop below ~8px.
L1 (Analytics): suppress fake «X–Y дн» range when n<2.
L2 (Analytics): drop zero-data series from price-history subtitle.
L7 (TopNav): replace hardcoded #6f8195 icon strokes with tokens.muted.
tsc + next build green. No FE test infra in tradein-mvp/frontend — parseAddress
verified via standalone harness (6/6). Backend data-contract items M1/M2/M3/H4
(source counter, ratio vs medians, bargain bases, cross-source dedup) filed
separately — not FE bugs. L6 (blueprint-grid axe-blindness) deferred: base
gradient + glass surfaces keep axe contrast «incomplete» regardless, so the
authoritative check stays the manual computed-color sweep.
- Опц. поля больше не шлют непроверенные дефолты в ценовую модель:
ТИП/РЕМОНТ → «Не указано»→undefined, БАЛКОН tri-state (null пока не
трогали) → undefined.
- Валидация у поля: красная рамка + сообщение под адресом/площадью,
показываются ВСЕ блокеры (не только первый); низ — только серверная ошибка.
- Копирайт «вы»: «если знаете»; кэш-строка → «ДАННЫЕ АКТУАЛЬНЫ ·
ДЕЙСТВИТЕЛЕН 24 Ч»; drawer без жаргона (убраны (asking)/POI).
- Даты сделок Росреестра → «янв 2026» (месяц, не фейк-день 01.01);
бренды источников без method-суффиксов (РОСРЕЕСТР/АВИТО/ДОМКЛИК);
«эталон»→«опорная цена».
- График истории цен: зум оси Y к реальному диапазону данных + динамические
подписи оси; легенда серии показывается только при наличии линии.
- 3 цены весомее (font-weight 400); медиана-маркеры range-баров → тонкий тик
(не выглядит как draggable-ручка); фото здания — синий duotone +
десатурация (не крадёт первый взгляд у цен).
- Таблицы аналогов/сделок: площадь/комнаты/этаж вынесены из адресной ячейки
в колонку ПАРАМЕТРЫ; ↗→› в СВОДКЕ.
Сознательно НЕ тронуто: перераспределение accent-синего (item 9) — axe уже 0,
а широкое снятие акцента конфликтует с «не ломать HUD-палитру»; монотонность
тиров срока продажи не форсируется (это были бы выдуманные данные).
axe-фоллоуап к #2066: «скоро» (КОЭФ.ЛОКАЦИИ) muted2→body2 (2,3→6,3:1 на badgeTint); LocationDrawer aria-hidden когда закрыт (off-canvas вне лендмарка → снимает axe «region»).
Browser audit of the live /trade-in/v2 found honesty + UX defects; fixes:
- 🔴 LocationDrawer: removed FABRICATED coef 0.87 / fake 'base×coef=result' / invented
POI factors / false 'OpenStreetMap' source. Now honest static 'ПОЯСНЕНИЕ К РАСЧЁТУ'
(real methodology) + 'коэф. локации в разработке' note. + Escape-to-close.
- 🔴 Map (ParamsPanel): removed static fixture price markers; renders REAL analog
markers via new mapMarkers(estimate) (projects lat/lon around target, cap 6).
- ParamsPanel: empty площадь/address now shows a validation message instead of
silently blocking submit; numeric placeholders made clearly placeholders ('напр. 54').
- HeroBar: dead 'КАК РАССЧИТАНО' button now opens the info drawer; building.png next/image
400 fixed (unoptimized + onError fallback — basePath wasn't applied to the optimizer URL).
- TopNav: real user from useMe() (name/initials/org/email) instead of fixture
'Андрей Петров'; dead Профиль/Настройки/Помощь dimmed/disabled; Выйти → logout().
- mappers: de-glue analog addresses ('69/2Геологическая'→'69/2 Геологическая', preserves
house letters like 14А); 'Продажи в доме'→'Продажи рядом' (radius data); торг sign fixed
(backend (start−last)/start positive=reduction → renders '−3.3%').
- HistoryView: '0 ЛОТА' → correct plural via pluralRu.
next build green; code-reviewer ✅ APPROVE.
FE-5, last FE wiring on the mappers pattern. Closes#2042.
- New hooks (lib/trade-in-api.ts): useEstimateHistory (GET /history), useGeocodeSuggest
(GET /geocode/suggest?q=&limit=, debounced, enabled>=3 chars); useQuota reused.
- types/trade-in.ts: EstimateHistoryItem (area_m2 string, median_price), GeocodeSuggestion.
- mappers.ts: mapCache(history) -> {rows: CacheRow[], kpis: CacheKpi[]} — per-row status
by 24h TTL; KPIs FE-derived from /history (всего/средняя цена/повторные %), NOT the
global /cache-stats.
- CacheView: real previous-estimates list. ParamsPanel: address autocomplete dropdown →
captures lat/lon into submit (cleared on manual edit). HeroBar: «Скачать PDF» → /estimate/
{id}/pdf, UUID-gated + safeUrl scheme check + disabled when no estimate. TopNav: «Мои
отчёты» from quota.used/history. page.tsx: invalidate history+quota on new estimate.
- Hydration-safe: PDF link gated behind a mounted flag (urlId is SSR-null/client-uuid).
- FE-1..4 untouched.
Verified: next build green (/v2 39.6 kB); tsx harness ran mapCache vs real /history (10
items: ВСЕГО 10 / СРЕДНЯЯ 8,85 млн / ПОВТОРНЫЕ 50%, statuses, null graceful).
code-reviewer APPROVE after hydration + reports-fallback fixes.
QA on real prod estimate 701795d8 surfaced two mapper defects:
- STREET_RE used JS \b which does NOT word-boundary Cyrillic → street token never
matched → object.address fell back to the bare house number ('14/3') and city
mis-parsed ('14/3, Россия'). Rewrote parseAddress to handle both OSM (house-first)
and DaData (city-first) orders; street keyword delimited by space, not \b. Now
'улица Яскина, 14/3' + city 'Екатеринбург'.
- days_on_market is null across prod lots → scatter-mini rendered empty. Active
analogs now fall back to 'days listed so far' = today-listing_date (deals still
require a real days_on_market — today-deal-date is not exposure). TODO BE-1.
Verified via tsx harness against the real estimate (25/25); next build green.
FE-1 foundation for #2036. app/v2/page.tsx now owns data: ParamsPanel form ->
useEstimateMutation, restore-by-id via window.location.search (mirrors v1, avoids
useSearchParams Suspense build break), sub-hooks useStreetDeals/useEstimateHouseAnalytics;
loading/empty/insufficient/error states (honest placeholders, no fixtures-as-real).
New components/trade-in/v2/mappers.ts: pure AggregatedEstimate(+street-deals/analytics)
-> fixture-shape transforms — 3 price tiers (asking/expected_sold/DKP), ranges, scatter,
7 source slots, summary. CV + per-source counts are FE-approx pending BE-1 (#2043).
ParamsPanel -> controlled form (onSubmit/isPending/error/initialValues). ResultPanel/
ObjectSummary/HeroBar/Footer accept a composite data prop defaulting to the existing
fixture (pixel-perfect markup unchanged). next build green (/v2 34 kB).
Прод-/trade-in/* обслуживает tradein-mvp/frontend (basePath=/trade-in,
standalone), а не главный frontend — v2 по ошибке попал в главный, где
прод его не отдаёт. Markup-only порт (fixtures, без API):
- компоненты v2 → tradein-mvp/frontend/src/components/trade-in/v2/
- роут src/app/v2/ (basePath даёт прод-URL /trade-in/v2)
- public/trade-in-v2/building.png
- HeroBar: <img> → next/image (плоский img не префиксит basePath → 404)
- удалён мёртвый v2 из главного frontend
next build (NEXT_PUBLIC_BASE_PATH=/trade-in) зелёный, роут /v2 prerendered.
Locked/no-access users (403/denied path/expired session/trial ended) were
trapped: NoAccessScreen had no logout control, and «Выйти» lived only inside
UserMenu, which does not render on the no-access screen. They could not switch
accounts without clearing browser basic-auth manually.
- Extract logout() into shared `lib/logout.ts` (single source of truth).
- UserMenu now imports it instead of a local copy (behaviour unchanged).
- NoAccessScreen renders an always-available «Выйти» accent button for every
variant.
Mirrored across both frontends (tradein-mvp/frontend + main frontend) to keep
the MIRROR invariant in sync.