feat(mera/b2c): витрина лэндинга переезжает с выдуманных чисел на замеренные #3243

Merged
bot-backend merged 4 commits from feat/b2c-landing-honest-copy into main 2026-08-29 18:25:20 +00:00
Collaborator

Последний шаг, ради которого делались метрики (#3228) и витрина реальных сделок (#3229): числа на лэндинге перестают быть реквизитом макета.

marketing-v3.ts удалён целиком. Корневая страница стала async-серверной, один раз фетчит /api/public/mera/stats и /api/public/mera/showcase и раздаёт данные секциям.

Таблица замен

Все числа перепроверены на проде лично, а не взяты со слов агента.

было (макет) стало основание
средняя ошибка 1,8% 14,5% (n=327) бэктест по ЕКБ против цен ДКП
83% в коридор 88% (n=327) + ширина коридора ±37% одно без другого вводит в заблуждение
«уверенность низкая у 325 из 327, высокая ни разу» замолчать это значило бы продать точность, которой нет
точность срока ±6 дн. снято deals.days_on_market заполнен 0 раз из 108 623. Сверки не существует
42 700 проверок, 3 180 сверены 1123 расчёта с мая 2026 «сверено» снято: контура сверки нет
−4,3% за месяц простоя −2,17% (n=3018, среди снижавших) у медианы своя выборка, и подпись это говорит
×2,4 дольше продаётся снято → «48,1% снижают цену» (n=6276) замер даёт ×1,04, и это уже с цензурой в пользу заявления
5 придуманных адресов в ленте реальные сделки из /showcase улиц и домов в данных нет — район, комнаты, площадь, квартал
3 придуманных раунда игры те же реальные сделки ошибка МЕРЫ считается по показанным трём, а не берётся общая

Что снято сверх таблицы

«Каждый такой прогноз мы потом сверяем с фактом сделки» — было на первом экране, в голосе продукта, рядом с ценой. Контура сверки не существует: trade_in_leads — 4 строки без полей исхода, payments пуст, связки объявления со сделкой в данных нет.

«Проверить за 30 секунд» — ветка сняла величину из бейджа, но ревьюеры нашли её ещё в четырёх видимых местах, включая липкую панель, которая висит на экране всегда. Замера времени заполнения формы никто не делал. Заменено на проверяемое: у формы шесть полей.

Фальшивая точка «объект на карте» — координат в витрине на тот момент не было, точка рисовалась из макета.

Честная деградация — проверена, а не заявлена

Ручка недоступна, вернула пустой объект, часть метрик, ноль строк витрины, stats: null, district: null — во всех случаях блок не рендерится. Ни нуля, ни прочерка, ни прошлогоднего значения. Ошибка загрузки не роняет страницу в 500. Ревьюер прогнал все шесть состояний фактически, а не мысленно.

Гейт переписан и починен

Старый гейт охранял «плейсхолдеры существуют ↔ noindex стоит». После честной замены его второй тест («ни один плейсхолдер не найден — зонд сломан») покраснел бы по построению, то есть за правильную работу. Заменён на сканер величин, вписанных руками мимо источника.

Ревьюер сломал первую версию нового гейта и был прав: он ловил формат числа, а не его происхождение. В охраняемый компонент прошли «Точность 9 из 10 прогнозов», «Проверено более 42700 квартир по всей России», «МЕРА в 2 раза точнее риелтора», «Работаем в 42 городах» — последнее дословно из таблицы расхождений старого гейта. Дыры закрыты, все четыре строки внесены в контроль как обязательные к отлову.

Отдельно: новый гейт проверял noindex слабее старого — переворот index: false → true оставлял его зелёным. Восстановлено не слабее прежнего, и теперь по двум файлам: у страницы своя метадата, которая перекрывает layout, чего старый гейт не видел вовсе.

Список сканируемых файлов больше не пишется руками — выводится из каталога компонентов, плюс проверка, что каждый импорт корня туда попал. Было 11 файлов, стало 17, и на первом же прогоне поймало живое нарушение.

Проверено

tsc · next lint · vitest · check:mera-public-isolation — чисто. Три ревьюера с разными линзами (честность / деградация / гейт), все minor. Каждая починка фальсифицирована руками: сломал → красный → вернул.

noindex НЕ снят

Индексация остаётся выключенной. Числа теперь честные, но открытие наружу — отдельное решение владельца.

Последний шаг, ради которого делались метрики (#3228) и витрина реальных сделок (#3229): числа на лэндинге перестают быть реквизитом макета. `marketing-v3.ts` удалён целиком. Корневая страница стала async-серверной, один раз фетчит `/api/public/mera/stats` и `/api/public/mera/showcase` и раздаёт данные секциям. ## Таблица замен Все числа перепроверены на проде лично, а не взяты со слов агента. | было (макет) | стало | основание | |---|---|---| | средняя ошибка **1,8%** | **14,5%** (n=327) | бэктест по ЕКБ против цен ДКП | | **83%** в коридор | **88%** (n=327) **+ ширина коридора ±37%** | одно без другого вводит в заблуждение | | — | «уверенность низкая у 325 из 327, высокая ни разу» | замолчать это значило бы продать точность, которой нет | | точность срока **±6 дн.** | **снято** | `deals.days_on_market` заполнен **0 раз из 108 623**. Сверки не существует | | **42 700** проверок, 3 180 сверены | **1123 расчёта** с мая 2026 | «сверено» снято: контура сверки нет | | **−4,3%** за месяц простоя | **−2,17%** (n=3018, среди снижавших) | у медианы своя выборка, и подпись это говорит | | **×2,4** дольше продаётся | **снято** → «48,1% снижают цену» (n=6276) | замер даёт ×1,04, и это уже с цензурой в пользу заявления | | 5 придуманных адресов в ленте | реальные сделки из `/showcase` | улиц и домов в данных нет — район, комнаты, площадь, квартал | | 3 придуманных раунда игры | те же реальные сделки | ошибка МЕРЫ считается по показанным трём, а не берётся общая | ## Что снято сверх таблицы **«Каждый такой прогноз мы потом сверяем с фактом сделки»** — было на первом экране, в голосе продукта, рядом с ценой. Контура сверки не существует: `trade_in_leads` — 4 строки без полей исхода, `payments` пуст, связки объявления со сделкой в данных нет. **«Проверить за 30 секунд»** — ветка сняла величину из бейджа, но ревьюеры нашли её ещё в четырёх видимых местах, включая липкую панель, которая висит на экране всегда. Замера времени заполнения формы никто не делал. Заменено на проверяемое: у формы шесть полей. **Фальшивая точка «объект на карте»** — координат в витрине на тот момент не было, точка рисовалась из макета. ## Честная деградация — проверена, а не заявлена Ручка недоступна, вернула пустой объект, часть метрик, ноль строк витрины, `stats: null`, `district: null` — во всех случаях блок **не рендерится**. Ни нуля, ни прочерка, ни прошлогоднего значения. Ошибка загрузки не роняет страницу в 500. Ревьюер прогнал все шесть состояний фактически, а не мысленно. ## Гейт переписан и починен Старый гейт охранял «плейсхолдеры существуют ↔ noindex стоит». После честной замены его второй тест («ни один плейсхолдер не найден — зонд сломан») покраснел бы **по построению**, то есть за правильную работу. Заменён на сканер величин, вписанных руками мимо источника. Ревьюер сломал первую версию нового гейта и был прав: он ловил **формат** числа, а не его происхождение. В охраняемый компонент прошли «Точность 9 из 10 прогнозов», «Проверено более 42700 квартир по всей России», «МЕРА в 2 раза точнее риелтора», «Работаем в 42 городах» — последнее дословно из таблицы расхождений старого гейта. Дыры закрыты, все четыре строки внесены в контроль как обязательные к отлову. Отдельно: новый гейт проверял `noindex` слабее старого — переворот `index: false → true` оставлял его зелёным. Восстановлено не слабее прежнего, и теперь по двум файлам: у страницы своя метадата, которая перекрывает layout, чего старый гейт не видел вовсе. Список сканируемых файлов больше не пишется руками — выводится из каталога компонентов, плюс проверка, что каждый импорт корня туда попал. Было 11 файлов, стало 17, и на первом же прогоне поймало живое нарушение. ## Проверено `tsc` · `next lint` · `vitest` · `check:mera-public-isolation` — чисто. Три ревьюера с разными линзами (честность / деградация / гейт), все minor. Каждая починка фальсифицирована руками: сломал → красный → вернул. ## noindex НЕ снят Индексация остаётся выключенной. Числа теперь честные, но открытие наружу — отдельное решение владельца.
bot-backend added 4 commits 2026-08-29 18:23:35 +00:00
Загрузка /api/public/mera/stats и /showcase для серверных секций лэндинга:
типы обеих ручек, честная деградация и форматирование величины вместе с
размером выборки.

Любой сбой (сервис недоступен, 500, не-JSON, пустой ответ, нет ключа) даёт
«величины нет», а не ноль: /stats отдаёт пустой набор, /showcase — null,
и ни одна из функций не бросает, чтобы отказ ручки стоил блока, а не всей
страницы. Число выходит наружу только через formatStat — вместе с sample_n
и note, поэтому потерять размер выборки по дороге в JSX нельзя.

Адрес серверного fetch — BACKEND_URL, тот же путь, которым Next уже ходит в
backend (rewrites в next.config.ts, 'internal SSR' в prod-компоуз), поэтому
без /trade-in: префикс добавляет Caddy для браузера, не бэкенд. Кэш —
revalidate 3600: пересчёт ночной, no-store жёг бы rate-limit ручки на всех
посетителей, кэш без срока показывал бы вчерашнее до деплоя.

Тесты двусторонние и фальсифицированы: подстановка нуля вместо null роняет
5 из 17, пустая витрина вместо null — 3 из 17.
Литералы из дизайн-макета заменены величинами из /stats и /showcase, а то,
чему нет входа в данных, снято вместе с блоком.

Что заменено:
- ошибка «1,8%» → 14,5% (n=327) с двумя оговорками рядом: сравнение идёт с
  объявлениями, активными на момент расчёта, а сделка прошлая; цена ДКП
  бывает занижена ради налога;
- «83% в диапазон» → 88% (n=327) неразрывно с шириной коридора ±37% в
  подписи и с третьей плиткой: уверенность «низкая» у 325 из 327;
- «±6 дн. точность по сроку» СНЯТА — deals.days_on_market пуст 0/108 623; на
  её месте медианная экспозиция активного объявления, без ± и без «точности»;
- «42 700 проверок, из них 3 180 сверены» → estimates_total; «сверено» снято
  (контура сверки прогноза со сделкой клиента нет);
- «−4,3% за месяц» → price_cut_median_pct_per_month (среди снижавших, своя
  выборка); «×2,4 дольше» СНЯТА (замер даёт ×1,04) → price_cut_share_pct;
- «14 объявлений / 47 дн.» → analogs_median и listing_age_median_days, прямо
  помеченные как пример по рынку, а не расчёт по адресу посетителя;
- 5 выдуманных строк сверок и 3 раунда игры → реальные сделки /showcase
  (район+комнаты+площадь+этаж, квартал вместо дня) с подписью прогона;
- цена во всех местах — SERVICE_PRICE_RUB из content.ts.

Отказ ручки стоит блока: пустой /stats снимает плитку, пустой /showcase —
таблицу, ленту и игру. Ни нуля, ни прочерка, ни прошлого значения.

Гейт #2904 переписан: он охранял «плейсхолдеры существуют» и покраснел бы по
построению за их снятие. Новый охраняет происхождение чисел — сканирует
компоненты на вписанные руками величины и проверяет заполненность записей
landing-facts.ts. Фальсифицирован: вписанное в AccuracyV3 «1,8% по 42 700
проверкам» роняет гейт тремя формами сразу. Сканер прикрыт контролем на
образцах — он же поймал уже существовавшую «150 ₽» в FreeResultV3.

robots/noindex не тронуты.
Ревьюер нашёл три дыры и воспроизвёл каждую на живом коде.

1. Сканер проверял ФОРМУ. Четыре маркетинговых утверждения, вписанные в
   DealsTickerV3, оставляли гейт зелёным 17/17: «9 из 10» (доля словами),
   «42700 квартир» (разделитель тысяч никакой + единица вне списка),
   «в 2 раза точнее» (кратность словами), «42 городах». Последнее — строка
   из таблицы расхождений СТАРОГО гейта, то есть новый пропускал ровно тот
   класс вранья, ради которого заводился. Добавлены четыре формы; эти же
   четыре строки внесены в контроль на инструмент — прежние шесть образцов
   повторяли то, что сканер и так знал, и покрытия не расширяли.

2. noindex перестал охраняться. `toContain("robots")` остаётся зелёным при
   `index: true` — проверено переворотом флага. Вернулась проверка значения
   (не слабее прежней `noindexIsOn()`), и не только в layout, но и в
   метаданных самой страницы, которые layout перекрывают.

3. Список файлов отставал от страницы: корень рендерил 15 компонентов,
   сканировались 11 — «возражения», шапка, подвал и sticky-CTA были вне
   охраны. Список больше не пишется руками: он выводится из каталога
   компонентов, а отдельная проверка требует, чтобы всё, что импортирует
   корень, попало в этот список (ловит компонент, положенный мимо каталога).

Гейт на этой ветке КРАСНЫЙ и красный по делу: расширенный список нашёл
«Проверить за 30 секунд» в HeaderV3, StickyCtaV3, ObjectionsV3 и
FreeCheckCard. Замера времени заполнения формы нет и не было — это записано
в landing-facts.ts и в StepsV3, откуда ту же фразу уже сняли. Правка текста —
вне участка этой задачи (файлы вёрстки правит соседняя ветка).
fix(mera/b2c): снять с витрины обещание времени и подписать величины измеренным
All checks were successful
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 57s
CI Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 9s
CI / openapi-codegen-check (pull_request) Has been skipped
2e82b0810f
«30 секунд» ушло из бейджа шага 1 как незамеренное, но осталось видимым
посетителю в четырёх других местах той же страницы: подпись под кнопкой формы,
CTA шапки, CTA блока возражений и липкая нижняя панель, которая висит на экране
всегда. Гейт витринных чисел этого не видел по построению — «сек» не было среди
единиц, а трёх файлов не было в списке сканируемых (обе дыры закрыты отдельно).
Замена — по смыслу: число полей формы (STEP1_FIELDS_LABEL) проверяется глазами,
и оба места берут его из landing-facts.ts, а не пишут у себя.

Медиана аналогов стояла под общей шапкой «пример по рынку Екатеринбурга», но
городом не ограничена: _ANALOGS_SQL идёт по всей таблице trade_in_estimates,
фильтр по city стоит только у экспозиции. Плюс Hero печатал у неё лишь размер
выборки и выбрасывал note «только расчёты, где аналоги нашлись» — отбор
выживших. Теперь у каждой величины своя подпись со своей границей и своя
оговорка рядом со значением; правило зафиксировано двумя проверками рендера
(обе краснеют на возврате к одной строке и на выброшенном note).

price_cut_share_pct считает долю ОБЪЯВЛЕНИЙ и только по Домклику — подпись
«столько продавцов снижают цену» приписывала величине единицу, которой её не
мерили. Один продавец ведёт несколько объявлений.

В ReportResult оставались три литерала «150 ₽» при живой SERVICE_PRICE_RUB:
цена обязана иметь один источник с офертой и политикой возврата.

Обещание «прогноз срока продажи» приведено к тому, что считается:
_estimate_days_on_market — медиана days_on_market аналогов, то есть сколько
похожие объявления УЖЕ висят. deals.days_on_market заполнен 0 раз из 108 623,
сверять прогноз срока не с чем (по этой же причине в ветке уже снято «±6 дн»).
bot-backend merged commit 8f0c2ce1f4 into main 2026-08-29 18:25:20 +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#3243
No description provided.