feat(mera): публичный лэндинг «МЕРА» — контент и вёрстка #2615

Merged
lekss361 merged 1 commit from feat/mera-landing into main 2026-08-01 20:03:56 +00:00
Owner

Первая публичная страница «Меры». Только фронтCaddyfile, DNS и бэкенд не тронуты: периметр на meraocenka.ru идёт отдельным PR, чтобы горячий Caddyfile не менялся параллельно с эпиком «единый вход» (#2597#2602#2608).

Заменяет заглушку из feat/mera-b2c-perimeter (48 строк «сервис скоро откроется») на страницу целиком: первый экран, как это работает, что человек получает, откуда данные, вопросы-ответы, подвал, страница обработки ПДн.

Починен живой баг, из-за которого лэндинг всё равно уводил бы людей на форму входа

RouteGuard регистрировал useEffect с router.push('/login?next=…') до early-return для публичных путей. Хуки выполняются всегда — поэтому на проде аноним на /mera-public получал фоновый GET /api/v1/me → 401 → редирект на логин, и «безусловный bypass» ниже по файлу до этого просто не доходил. То есть заглушка в текущем виде была нерабочей, и это выяснилось бы уже после открытия домена.

Guard разделён: RouteGuard теперь смотрит только pathname, а весь закрытый контур (useMe, RBAC, экраны отказа) вынесен в GuardedRoute и подключён через next/dynamic. Это точка разрыва графа импортов, а не только логическая развилка — иначе анонимный посетитель продолжал бы скачивать B2B-код.

Проверено на живой странице: запросов к /api/**ноль.

Обещания приведены в соответствие с тем, что продукт делает

Отдельная линза ревью сверяла каждое утверждение с кодом. Нашла 1 critical и 4 high — все про обещания, которых продукт не выполняет:

Было Почему убрано
«Отчёт в PDF» GET /api/v1/trade-in/estimate/{id}/pdf owner-scoped, анониму 401 by design
«список объектов, на которых построен расчёт» план B2C прямо запрещает показывать анониму сырые объявления конкурентов
6 городов как равнозначные сбор вне Екатеринбурга выключен (миграция 179, enabled=false), данных единицы. Теперь честно: полное покрытие — Екатеринбург, по области меньше
«в расчёте два типа данных» умалчивался третий — чужие оценочные модели, реально двигающие итоговую цифру (estimator.py, IMV/Yandex blend)
страница ПДн обещала удаление данных механизма нет

Тексты и правовые формулировки собраны в content.ts одним местом, рядом со ссылками на код, который их подтверждает — чтобы обещание и реальность разъезжались заметно, а не молча. Финальная редакция privacy помечена как задача юриста, а не инженерное решение.

Форма адреса — честная заглушка, и это вынужденно

Живого автокомплита сейчас быть не может: _PUBLIC_PATHS «Меры» (rbac.py) открывает анониму только health/docs/login/logout и анонимный чат. /api/v1/geocode/suggest и /api/v1/trade-in/estimate отдают анониму 401. Открывать их до анти-абуза (этап 2 плана B2C) план называет категорическим запретом — без квот и rate-limit это пайплайн на десятки секунд с пятью внешними сервисами, доступный кому угодно.

Поэтому форма честно сообщает, что произойдёт, и не изображает работу, которой нет. Поведение переключается флагом PUBLIC_ESTIMATE_ENABLED; рядом с флагом — гейт из трёх условий (текст privacy, согласие до записи, наличие пути удаления), без которых его включать нельзя.

Проверено живьём, не по отчёту агентов

  • 360px и 390px — горизонтального переполнения нет; единственный элемент за экраном — skip-link, так и задумано;
  • один h1, иерархия H1→H2→H3 без пропусков, landmark-разметка (header/main/section×5/footer);
  • внешних ресурсов ноль — ни CDN, ни шрифтов, ни картинок с чужих доменов;
  • ноль запросов к /api/** со страницы;
  • tsc --noEmit чист;
  • цели нажатия в подвале доведены до 44px; инлайновая ссылка внутри предложения намеренно оставлена как есть — WCAG 2.5.8 такие исключает, а раздувать строку текста хуже.

NB ревьюеру

.claude/rules/ui-*.md по frontmatter paths: матчат frontend/** — корневой фронт Site Finder, а не tradein-mvp/frontend. Формально этот rulebook сюда не распространяется; страница живёт в дизайн-системе v2/tokens.ts. Tailwind в этом фронте отсутствует вовсе (вёрстка — CSS-модуль + inline-стили), поэтому совет «статический стиль → Tailwind utility» неприменим.

Test plan

  • tsc --noEmit — чисто
  • Живой рендер на 360/390px: переполнения нет, семантика и landmarks на месте
  • Ноль сетевых запросов в закрытый контур (замер через PerformanceObserver)
  • Тексты сверены с кодом: PDF в тексте страницы не встречается
  • Визуальная приёмка владельцем — тексты и тон
  • Периметр и открытие наружу — отдельный PR

Дальше

Периметр на meraocenka.ru: allowlist (страница, /_next/static, /_next/image, /health), всё непереч��сленное — 404, без users.caddy.snippet. DNS теперь готов полностью: meraocenka.ru, merahome.ru, meraotsenka.ru и все три www указывают на 46.173.16.127 (проверено авторитативно у ns1.beget.ru). merahome.ru и meraotsenka.ru → 301 на канонический meraocenka.ru.

Отдельно учесть при периметре: next.config.ts редиректит //v2, поэтому корень публичного домена должен вести на лэндинг, а не выкидывать посетителя в B2B-приложение.

Первая публичная страница «Меры». **Только фронт** — `Caddyfile`, DNS и бэкенд не тронуты: периметр на `meraocenka.ru` идёт отдельным PR, чтобы горячий `Caddyfile` не менялся параллельно с эпиком «единый вход» (#2597 → #2602 → #2608). Заменяет заглушку из `feat/mera-b2c-perimeter` (48 строк «сервис скоро откроется») на страницу целиком: первый экран, как это работает, что человек получает, откуда данные, вопросы-ответы, подвал, страница обработки ПДн. ## Починен живой баг, из-за которого лэндинг всё равно уводил бы людей на форму входа `RouteGuard` регистрировал `useEffect` с `router.push('/login?next=…')` **до** early-return для публичных путей. Хуки выполняются всегда — поэтому на проде аноним на `/mera-public` получал фоновый `GET /api/v1/me` → 401 → редирект на логин, и «безусловный bypass» ниже по файлу до этого просто не доходил. То есть заглушка в текущем виде была нерабочей, и это выяснилось бы уже после открытия домена. Guard разделён: `RouteGuard` теперь смотрит только `pathname`, а весь закрытый контур (`useMe`, RBAC, экраны отказа) вынесен в `GuardedRoute` и подключён через `next/dynamic`. Это точка разрыва **графа импортов**, а не только логическая развилка — иначе анонимный посетитель продолжал бы скачивать B2B-код. Проверено на живой странице: запросов к `/api/**` — **ноль**. ## Обещания приведены в соответствие с тем, что продукт делает Отдельная линза ревью сверяла каждое утверждение с кодом. Нашла 1 critical и 4 high — все про обещания, которых продукт не выполняет: | Было | Почему убрано | |---|---| | «Отчёт в PDF» | `GET /api/v1/trade-in/estimate/{id}/pdf` owner-scoped, анониму 401 by design | | «список объектов, на которых построен расчёт» | план B2C прямо запрещает показывать анониму сырые объявления конкурентов | | 6 городов как равнозначные | сбор вне Екатеринбурга выключен (миграция 179, `enabled=false`), данных единицы. Теперь честно: полное покрытие — Екатеринбург, по области меньше | | «в расчёте два типа данных» | умалчивался третий — чужие оценочные модели, реально двигающие итоговую цифру (`estimator.py`, IMV/Yandex blend) | | страница ПДн обещала удаление данных | механизма нет | Тексты и правовые формулировки собраны в `content.ts` одним местом, рядом со ссылками на код, который их подтверждает — чтобы обещание и реальность разъезжались заметно, а не молча. Финальная редакция privacy помечена как задача юриста, а не инженерное решение. ## Форма адреса — честная заглушка, и это вынужденно Живого автокомплита сейчас быть не может: `_PUBLIC_PATHS` «Меры» (`rbac.py`) открывает анониму только `health`/`docs`/`login`/`logout` и анонимный чат. `/api/v1/geocode/suggest` и `/api/v1/trade-in/estimate` отдают анониму **401**. Открывать их до анти-абуза (этап 2 плана B2C) план называет категорическим запретом — без квот и rate-limit это пайплайн на десятки секунд с пятью внешними сервисами, доступный кому угодно. Поэтому форма честно сообщает, что произойдёт, и не изображает работу, которой нет. Поведение переключается флагом `PUBLIC_ESTIMATE_ENABLED`; рядом с флагом — гейт из трёх условий (текст privacy, согласие до записи, наличие пути удаления), без которых его включать нельзя. ## Проверено живьём, не по отчёту агентов - **360px и 390px** — горизонтального переполнения нет; единственный элемент за экраном — skip-link, так и задумано; - один `h1`, иерархия `H1→H2→H3` без пропусков, landmark-разметка (`header/main/section×5/footer`); - **внешних ресурсов ноль** — ни CDN, ни шрифтов, ни картинок с чужих доменов; - **ноль запросов к `/api/**`** со страницы; - `tsc --noEmit` чист; - цели нажатия в подвале доведены до 44px; инлайновая ссылка внутри предложения намеренно оставлена как есть — WCAG 2.5.8 такие исключает, а раздувать строку текста хуже. ## NB ревьюеру `.claude/rules/ui-*.md` по frontmatter `paths:` матчат `frontend/**` — корневой фронт Site Finder, а **не** `tradein-mvp/frontend`. Формально этот rulebook сюда не распространяется; страница живёт в дизайн-системе `v2/tokens.ts`. Tailwind в этом фронте отсутствует вовсе (вёрстка — CSS-модуль + inline-стили), поэтому совет «статический стиль → Tailwind utility» неприменим. ## Test plan - [x] `tsc --noEmit` — чисто - [x] Живой рендер на 360/390px: переполнения нет, семантика и landmarks на месте - [x] Ноль сетевых запросов в закрытый контур (замер через PerformanceObserver) - [x] Тексты сверены с кодом: `PDF` в тексте страницы не встречается - [ ] Визуальная приёмка владельцем — тексты и тон - [ ] Периметр и открытие наружу — отдельный PR ## Дальше Периметр на `meraocenka.ru`: allowlist (страница, `/_next/static`, `/_next/image`, `/health`), всё непереч��сленное — 404, без `users.caddy.snippet`. DNS теперь готов полностью: `meraocenka.ru`, `merahome.ru`, `meraotsenka.ru` и все три `www` указывают на 46.173.16.127 (проверено авторитативно у `ns1.beget.ru`). `merahome.ru` и `meraotsenka.ru` → 301 на канонический `meraocenka.ru`. Отдельно учесть при периметре: `next.config.ts` редиректит `/` → `/v2`, поэтому корень публичного домена должен вести на лэндинг, а не выкидывать посетителя в B2B-приложение.
lekss361 added 1 commit 2026-08-01 20:02:16 +00:00
feat(mera): публичный лэндинг «МЕРА» — контент и вёрстка
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m7s
6ac4ad9867
Только фронт. Caddyfile, DNS и бэкенд не тронуты: периметр и домен
meraocenka.ru делаются отдельным PR, чтобы горячий Caddyfile не менялся
параллельно с эпиком «единый вход».

Заменяет заглушку из feat/mera-b2c-perimeter (48 строк «скоро откроется»)
на полноценную страницу: первый экран, как это работает, что человек
получает, откуда данные, вопросы-ответы, подвал и страница обработки ПДн.

## Починен живой баг, из-за которого лэндинг уводил бы людей на форму входа

RouteGuard регистрировал useEffect с router.push('/login') ДО early-return
для публичных путей. Хуки выполняются всегда, поэтому на проде аноним на
/mera-public получал фоновый GET /api/v1/me → 401 → редирект на логин, и
«безусловный bypass» до этого просто не доходил.

Guard разделён: RouteGuard теперь только смотрит pathname, а весь закрытый
контур (useMe, RBAC, экраны отказа) вынесен в GuardedRoute и подключён через
next/dynamic — это точка разрыва графа импортов, а не только логическая
развилка. Проверено на живой странице: запросов к API — НОЛЬ.

## Обещания приведены в соответствие с продуктом

Ревью по честности нашло 1 critical и 4 high — всё это обещания, которых
продукт не выполняет. Убрано или переписано:

- «Отчёт в PDF» — ручка owner-scoped, анониму отдаёт 401 by design;
- «список объектов, на которых построен расчёт» — план прямо запрещает
  показывать анониму сырые объявления конкурентов;
- шесть городов подавались как равнозначные, хотя сбор вне Екатеринбурга
  выключен (миграция 179, enabled=false) и данных единицы. Теперь честно:
  полное покрытие — Екатеринбург, по области данных меньше;
- «в расчёте два типа данных» умалчивало третий — чужие оценочные модели,
  которые реально двигают итоговую цифру (estimator.py, IMV/Yandex blend);
- страница ПДн обещала удаление данных, механизма которого нет.

Тексты и правовые формулировки вынесены в content.ts одним местом, рядом с
ссылками на код, который их подтверждает. Финальная редакция privacy —
за юристом, это помечено в файле.

## Форма адреса — честная заглушка, и это вынужденно

Живого автокомплита быть не может: _PUBLIC_PATHS «Меры» (rbac.py) открывает
анониму только health/docs/login/logout и анонимный чат. /geocode/suggest и
/trade-in/estimate отдают анониму 401. Открывать их до анти-абуза (этап 2
плана B2C) прямо запрещено планом. Форма честно говорит, что произойдёт,
и не изображает работу, которой нет. Переключается флагом
PUBLIC_ESTIMATE_ENABLED, рядом с ним — гейт из трёх условий.

## Проверено живьём, не по отчёту

- 360px и 390px: горизонтального переполнения нет (единственный элемент за
  экраном — skip-link, так и задумано);
- один h1, иерархия H1→H2→H3 без пропусков, landmark-разметка;
- внешних ресурсов ноль — ни CDN, ни шрифтов, ни картинок с чужих доменов;
- ноль запросов к /api/** со страницы;
- tsc --noEmit чист.

NB для ревьюера: .claude/rules/ui-*.md по frontmatter paths: матчат
frontend/**, то есть корневой фронт Site Finder, а не tradein-mvp/frontend.
Здесь применяется дизайн-система v2/tokens.ts. Tailwind в этом фронте нет.
lekss361 merged commit 5d26e38fe8 into main 2026-08-01 20:03:56 +00:00
lekss361 deleted branch feat/mera-landing 2026-08-01 20:03:56 +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#2615
No description provided.