273 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| f3e05e1907 |
Merge branch 'main' into fix/mera-keep-floor-in-draft
All checks were successful
CI Trade-In / changes (pull_request) Successful in 13s
CI / changes (pull_request) Successful in 16s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 2m21s
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
|
|||
| 619e38b1e7 |
Merge branch 'main' into fix/mera-ticker-context
All checks were successful
CI Trade-In / changes (pull_request) Successful in 12s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 14s
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 / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m15s
|
|||
| be59ca4452 |
Merge branch 'main' into fix/mera-public-contrast
All checks were successful
CI Trade-In / changes (pull_request) Successful in 12s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 14s
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 1m20s
|
|||
| df53ff5a5f |
лендинг: этаж перестал теряться на переходе к проверке
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 12s
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 1m5s
Этаж — обязательное поле карточки: без него форма не отправляется. При этом
saveDraft его не клал, а на целевой странице поля этажа нет вовсе. Комментарий
объяснял это тем, что «повторно набирать одно короткое поле дешевле, чем
хранить лишнее» — довод неверен: переспросить негде, и обязательное поле
работало чистой помехой.
Схема бэкенда этаж принимает (TradeInEstimateInput.floor), и на цену он влияет
(первый и последний этаж). Хранится теперь по тому же доводу, что и состояние:
выбросить уже полученный ответ и спросить второй раз хуже, чем донести.
Тест — через САМУ ФОРМУ, и это не формальность. Первая попытка защитить правку
звала saveDraft({floor}) напрямую: она проверяла round-trip хранилища и
оставалась зелёной, когда из FreeCheckCard убирали передачу этажа, то есть
ровно при возврате чинимого дефекта. Сторож, который не сторожит. Теперь форма
заполняется и отправляется целиком; снятие проводки даёт «expected undefined to
be 7/16» — красное по значению.
Плюс три случая на само хранилище: старый черновик без поля переживает выкатку,
нестроковое значение выпадает не унося остальное.
|
|||
| 8aac93ee9b |
fix(mera-public): контраст WCAG AA на публичном лендинге
All checks were successful
CI Trade-In / changes (pull_request) Successful in 14s
CI / changes (pull_request) Successful in 17s
CI Trade-In / backend-tests (pull_request) Has been skipped
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 / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 2m3s
axe 30.08 нашёл 32 узла color-contrast (все serious). Причина одна на все: пары «цвет × фон» подбирались глазами, а числа в комментариях писались по памяти — шапка b2c-tokens.ts утверждала «onAccent на accentDeep 4.6:1», тогда как реальный замер 4.41, то есть главная кнопка призыва не проходила норму, а документация уверяла, что проходит. Затемнены четыре токена до запаса на худшем светлом фоне (surfaceSoft, он темнее pageBg): accentDeep 4.41→4.97, muted 4.45→5.36, success 4.04→4.96, danger 4.60→5.23. Оттенок сохранён, фирменный вид не меняется. Отдельный класс — светлый токен на тёмной плашке surfaceDark, где шкала перевёрнута: метки тикера красились muted (2.7) и accentText (2.85), стрелка между прогнозом и фактом — линейным #2c3a3f (1.50). Переведены на mutedDark (6.2) и accent (6.24). Заодно убраны сырые hex #9fb0b4 (2.24 на светлом — .artSoon, .gameScale, .repPaidFine, .artFootnote; 7.84 на тёмном) — палитра запрещает hex в CSS именно потому, что мимо токена величина не считается никаким аудитом. Регресс ловится без браузера: contrast-tokens.test.ts считает яркость по формуле WCAG прямо на значениях токенов и проверяет ОБА фона страницы, плюс сторож на новые сырые hex в CSS. Проверен фальсификацией: возврат muted к #667579 красит тест. |
|||
| 7d6caed5fb |
МЕРА: лента над первым экраном получает контекст разброса
All checks were successful
CI Trade-In / changes (pull_request) Successful in 12s
CI / changes (pull_request) Successful in 19s
CI Trade-In / backend-tests (pull_request) Has been skipped
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 / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m45s
Над лентой стояло «медианное расхождение с ценой ДКП — 14,5 % по 327 сделкам», а в самой ленте 5 строк из 20 расходились больше чем на 30 % (75,7 · 63,6 · 47,4 · 41,4 · 38,5). Первое, что видел посетитель страницы про точность, — крупный промах без единой цифры контекста. Промахи не прячутся: строки витрины отобраны по ПОЛНОТЕ и СВЕЖЕСТИ (_sort_key в app/tasks/landing_showcase_deals.py), отбор по величине ошибки был дефектом и снят. Лечится контекстом — под лентой печатается тот же разброс ПОКАЗАННЫХ строк, что уже стоит под таблицей сверок: медиана модуля и худшая, обе из shownSpread() (deal-view.ts), второго расчёта не заводится. Вывода вида «зато обычно точно» в подписи нет: он протух бы на первом пересчёте витрины, а два числа рядом — нет. Разметка: бегущая часть выделена в .tickerStrip — position/overflow нужны только ей, иначе абсолютный бейдж растянулся бы и на подпись. Проверка (landing-v3-render.test.tsx): те же строки с худшей и без неё дают РАЗНЫЕ числа в подписи — она сосчитана по показанному, а не вписана. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| 00f177df03 |
лендинг: имя боевого сервера убрано из пользовательской копии
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
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 58s
«Симуляция пробы покрытия на боевой базе (poincare, 30.08.2026)» доезжала до экрана: CITY_COVERAGE_SOURCE собирается в coverage-copy.ts и рендерится в EstimateFlow.tsx:323. На первом экране лендинга строки не видно — она появляется в форме проверки, после выбора города, поэтому grep по отрендеренной главной давал ноль и дефект выглядел несуществующим. Хостнейм посетителю не сообщает ничего: «на боевой базе» несёт весь смысл. Убран из обеих строк-источников (сверка бэктеста и проба покрытия) и из комментария в статьях. Гейт смотрит в ИСХОДНИК, а не в вывод: какой путь рендера показывает строку сегодня, знать не нужно — завтра он будет другим. Список запретов явный, без эвристики «похоже на хостнейм»: она ловила бы domclick.ru и avito.ru в текстах про источники данных. gendsgn.ru в список НЕ входит — это публичный домен B2B, на который лендинг ссылается кнопкой «Для бизнеса». Гейт проверен фальсификацией: возврат хостнейма даёт красное с указанием landing-facts.ts:207, а не пустой список. |
|||
| 5f7ac0d50f |
витрина: печатать разброс показанных строк рядом с медианой всей сверки
All checks were successful
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 9s
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m18s
Посетитель видел 20 строк сверки и не мог понять, где они лежат относительно выборки: первой в таблице стоит сделка с расхождением +75,7 %, а медиана по всем 327 — 14,5 %. Порядок строк при этом верный и менять его нельзя — отбор идёт по полноте и свежести, а не по величине ошибки (отбор по ошибке был дефектом и уже убран). Поэтому не переупорядочиваем, а добавляем контекст: медиана модуля расхождения показанных строк, сколько из них в пределах порога и худшая — рядом с медианой всей сверки. Замер на проде 30.08.2026: показанные 11,5 % против 14,5 % по всей выборке, 12 из 20 в пределах 20 %, худшая 75,7 %. Считается на фронте из тех же объектов, которые рисует таблица, — второе место подсчёта рано или поздно отстало бы от строк на экране. Своей формулировки «чуть точнее» в подписи нет: она протухнет на первом же пересчёте витрины, а два числа рядом не протухают. Порог вынесен в WITHIN_PCT рядом с фильтром: гейт витринных чисел справедливо покраснел на вписанных руками «20 %» при <= 20 в коде. |
|||
| 79fc8c45ff |
feat(mera/estimate): предзаполнять этажность, год и тип дома из справочника (UI) (#3259)
Some checks failed
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / perimeter-smoke (push) Blocked by required conditions
Deploy Trade-In / deploy-status (push) Blocked by required conditions
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 2m5s
Deploy Trade-In / test (push) Successful in 4m9s
Deploy Trade-In / build-backend (push) Has been cancelled
|
|||
| 2eebc680a6 |
Merge remote-tracking branch 'origin/fix/audit-accuracy-window' into fix/audit-all
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
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 / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m2s
CI Trade-In / backend-tests (pull_request) Successful in 4m55s
|
|||
| b343f84784 | Merge remote-tracking branch 'origin/fix/audit-faq-contradiction' into fix/audit-all | |||
| bb4a6d220f |
fix(mera/b2c): увести к ответу проверки и показать ожидаемое качество по городу до нажатия
Экран проверки дорисовывал результат НИЖЕ формы и никуда не уводил: на 375 px человек после нажатия видел ту же форму, а заголовок ответа оставался за нижней кромкой — нажатие читается как «ничего не произошло». Ответ теперь получает фокус и прокрутку; анимация прокрутки спрашивается у prefers-reduced-motion, той же медиа-функции, что глушит остальную анимацию витрины. Фокус здесь не украшение: без него клавиатурный пользователь остаётся на кнопке и следующим Tab уходит в обход ответа, а живая область объявляет текст, но не перемещает точку ввода. Второе: в дропдауне девять городов, и они не равны по данным, но узнать об этом можно было только ПОСЛЕ нажатия. Замер на проде (30.08.2026, симуляция когорты самой ручки /coverage по случайным адресам активных объявлений, собственный адрес исключён): доля проверок с выборкой не ниже городского порога — ЕКБ 83 %, Верхняя Пышма 70, Серов 58, Нижний Тагил 52, Первоуральск 50, Каменск-Уральский 45, Среднеуральск 39, Берёзовский 38, Ревда 16 (по 120 адресов, Среднеуральск — 56, столько их там есть). Величина и её источник лежат в landing-facts.ts, формулировка — в coverage-copy.ts, в компонент не вписано ни одного числа. Города из списка НЕ убраны: систематического отказа нет ни в одном (пустая когорта у худшего — 11 случаев из 100), разница между ними количественная, и её честнее назвать числом, чем снятием опции. Счёт по listings.city, дающий ноль по трём городам-спутникам, здесь не годится — колонка хранит город свипа скрейпера, а не геокод объявления (разбор над _CITY_CENTROIDS_DEG в trade_in.py). Тест требует замера на каждый город из OBLAST_CITIES — добавить город в дропдаун, не измерив его, теперь нельзя. |
|||
| 50a3a29b72 |
fix(mera/b2c): блок точности называет то окно, которое измерено
Подпись обещала «сделки с июня 2025 года», а выборка бэктеста берётся ORDER BY id DESC LIMIT :sample (backend/scripts/backtest_estimator.py, _SAMPLE_SQL) — это последние по порядку загрузки строки, а не срез окна. Проверка на проде 30.08.2026: у всех 327 сделок deal_date = 2026-04-01, то есть один квартал; проверены оба варианта запуска (без --city и с --city Екатеринбург) — результат одинаковый. Случайной выборки в скрипте нет, поэтому чинится подпись, а не замер: числа те же, окно названо своё. Заодно: - доля выборки на витрине (5,5 % сделок квартала). Знаменатель — из ТОГО ЖЕ окна (5 954 годных сделки ЕКБ за II кв 2026), а не 24 333 за всё окно с июня 2025: доля от непокрытого окна повторила бы ту же ошибку; - дата замера выведена рядом с числами: регулярного пересчёта у них нет, без даты они стареют молча; - __tests__/backtest-freshness.test.ts краснеет, когда замеру больше BACKTEST_MAX_AGE_DAYS (100 дн. = квартальная пачка Росреестра + запас). Фальсифицирован: дата 2026-01-05 → красный с текстом «замеру 236 дн.». Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| 808a2e12e2 |
fix(mera/лэндинг): убрать обещание про снятие объявлений и сроки без замера
FAQ обещал, что мы «отслеживаем снятие объявлений с публикации» и считаем по этому расхождение прогноза с реальностью. Ни того, ни другого нет: расхождение считают landing_showcase_deals.py и backtest_estimator.py, оба берут только цену ДКП; delisted/relisted listing_source_snapshot.py не пишет намеренно (не выводимы при покрытии обхода 10-35%), на проде 0 таких строк в listing_source_events, deals.days_on_market заполнена 0 из 108 623. Ответ приведён к тому, что делается, и прямо говорит, что снятие сделкой не считаем — двумя блоками выше AccuracyV3 по той же причине зовёт величину «экспозицией АКТИВНОГО объявления». Два срока без источника убраны, а не заменены числом: «против двух месяцев вашей жизни» (CostOfErrorV3) и «не зависли на полгода» (HeroV3). Измеренная экспозиция считается по тем, кто ещё висит, и сроком продажи не является — подставлять её на место этих сроков значило бы подменить величину. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| 66660bd366 |
fix(mera/b2c): на мобильном липкий хром съедал 42% экрана
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m38s
CI / openapi-codegen-check (pull_request) Successful in 2m26s
CI Trade-In / backend-tests (pull_request) Successful in 5m28s
CI / backend-tests (pull_request) Successful in 17m44s
Жалоба «много всплывашек, экрана не видно» — это не всплывашки, а постоянный
хром: на 375×812 липкая шапка (235 px) и фиксированная нижняя панель (109 px)
вместе занимали 42% экрана и отъедали их при любой прокрутке. Замер тем же
способом (getBoundingClientRect по элементам с position fixed/sticky): было
366 px = 45,1% высоты экрана, стало 119,5 px = 14,7%. На 1280 — 135,5 px до и
после, без изменений.
Шапка на узком экране сведена к одной строке (лого + «Для бизнеса»):
- навигация по секциям скрыта — четыре пункта переносились в две строки, а
на мобильном к секциям всё равно доезжают прокруткой; «Статьи» и «МЕРА для
бизнеса» остаются ссылками в подвале, «Продажа под ключ» и на десктопе
неактивная заглушка;
- CTA скрыт — он вёл туда же, куда нижняя панель, и два одинаковых призыва
на одном экране это и есть жалоба; панель осталась, шапка уступила.
Селектор города (CityPicker) удалён совсем, а не спрятан: он не владел своим
выбором — состояние жило внутри компонента и никуда не отправлялось, — тогда
как гео-гейт расчёта стоит на городе из формы (FreeCheckCard, #2576). Два
места, задающих одно значение, из которых работает одно, — дефект сам по себе;
то же правило уже записано в InnerHeader. Граница покрытия, ради которой
выпадашку открывали, стоит текстом в герое.
Нижняя панель на узком экране — только кнопка: её строка переносилась на две-
три и делала панель втрое выше кнопки. То же обещание остаётся словами героя.
Шаг медиазапроса — 1000 px, а не общий для файла 720: на 720 шапка макета всё
ещё переносится в две строки (замерено: 109 px хрома), то есть возвращать её
там значило бы вернуть тот же дефект планшету.
|
|||
| a5ca2a326a |
feat(mera/b2c): в карточке игры схема улицы сделки, а не пятно района
Карта показывала ПОЛИГОН РАЙОНА — десятки квадратных километров, на которых одинаково выглядят сделка у парка и сделка у ТЭЦ. Схема улиц уже приезжает строкой витрины (`street_scheme`), рисовать её было нечем. `StreetMapV3` — серверный компонент, инлайновый <svg>, НИ ОДНОГО внешнего запроса: тайлы отдали бы провайдеру IP посетителя и то, что он смотрит, на странице, которая обещает «без звонков и регистрации» (правило шапки layout.tsx, по нему же в дереве self-hosted шрифты). Слои снизу вверх: плашка и приглушённая сетка (уже были), вода, фоновые дороги толщиной по классу OSM, целевая улица акцентом поверх мягкого ореола, подписи соседних улиц моноширинным. DealMapV3 ОСТАЁТСЯ и не тронут. Улица матчится у 92.1% строк витрины, у остальных схемы нет — там по-прежнему район. Ветвление стоит в GuessGameV3, чтобы обе карты остались тупыми: каждая рисует то, что ей дали, и ни одна не подставляет вместо отсутствующих данных правдоподобное. ЧТО КАРТА ГОВОРИТ О СЕБЕ. Подпись — «<УЛИЦА> · ДОМ НЕ ИЗВЕСТЕН». Адрес сделки уровня улицы, номер дома известен у 2.7% сделок, координата окна — центроид улицы. Слов «объект» и «адрес» в карточке нет; тест держит и то и другое. Точку дома поставить нечем и по построению: в `street_scheme` нет ни констант проекции, ни координат окна. ЧЕГО НЕ РИСУЕМ. Зданий: `cad_buildings` — 18 307 контуров на город, в плотном центре 51 здание на радиус 450 м, где их в разы больше; нарисованная застройка заявляла бы полноту, которой нет. Улицы — фильтрованная выгрузка «источников шума», дворовых и служебных проездов в ней нет, поэтому фоновые дороги приглушены: это контекст, а не план квартала. КАДР — `slice`, а не `meet`: окно квадратное, карточка нет, и `meet` оставил бы поля по бокам. Обрезка безопасна ровно потому, что целевая улица лежит в ЦЕНТРЕ окна по построению; обрезаются края с частью подписей соседей. ДОСТУПНОСТЬ — как у DealMapV3, `aria-hidden`: всё, что схема сообщает, стоит рядом текстом. `role="img"` заставил бы прочитать то же самое дважды, а названия соседних улиц без их взаимного расположения ничего не значат. ЦВЕТА — токенами: `--b2c-water` (вода отдельным оттенком, иначе склеивается с дорогами) и `--b2c-muted-dark` (подписи на тёмной плашке: `muted` даёт там 3.2:1). Литералов в CSS не добавлено. Подпись объекта лежит ПОВЕРХ карты, и карта теперь доходит до краёв — добавлено затемнение под ней и плашка под подписью карты; без них белый текст пересекался с подсвеченной улицей. Псевдоэлемент позиционирован, поэтому детям `.gameMapBottom` задан `position: relative` — иначе затемнение красится поверх текста и стирает его (поймано скриншотом, а не рассуждением). Тесты (каждый сломан вручную и покраснел): кадр из данных и `slice`; ореол шире линии и по одному на путь; иерархия дорог и незнакомый класс не дают нулевой толщины; вода отделена; подпись называет улицу и говорит про дом; `aria-hidden`; ветвление карточки в обе стороны. Проверено глазами на dev-сервере, стаб — реальные строки прода плюс схемы, собранные той же проекцией по геометрии gendesign: улица сматчилась; не сматчилась (виден район); полей схемы нет вовсе (тот же район, без падения). |
|||
| 4e3279d448 |
fix(mera/b2c): студия подписывалась как «0-к» на витрине сделок
Увидел на скриншоте карточки игры: «0-к, 25,9 м²». Замер на проде — 3 строки витрины из 20 имеют rooms = 0, то есть каждая шестая карточка так и выглядит. «0-к» читается как ошибка выгрузки, а не как тип квартиры. Студией её называет и наш собственный бэктест (per_rooms.label в scripts/backtest_estimator.py), и рынок. Тест двусторонний и фальсифицирован: возврат «0-к» для rooms=0 красит его по значению, обратная правка — снова зелено. |
|||
| 272d807f55 |
feat(mera/b2c): в карточке игры настоящая карта вместо декорации — и подпись, которую данные выдерживают
Блок `gameMap` рисовал CSS-сетку и два прямоугольника-«дороги», а подпись говорила «ОБЪЕКТ · РАЙОН» — то есть заявляла местоположение объекта. Географии там не было вовсе. Что сделано: `DealMapV3` — инлайновый <svg> по статике `ekb-districts.ts`, без единого внешнего запроса (то же правило, по которому в этом дереве self-hosted шрифты). Район сделки — заливка и обводка акцентом, соседние — контур; кадр по bbox района с запасом, не по городу: в городском кадре район занимает несколько процентов площади. Точка — по `project(lon, lat)` координаты витрины. ПОДПИСЬ. Координата в данных — ЦЕНТРОИД УЛИЦЫ, не дом: 34 021 сделка, 34 017 с координатой, различных точек 991, номер дома известен у 2.7% (прод, 29.08.2026). Поэтому карта подписана «СЕРЕДИНА УЛИЦЫ, НЕ ДОМ · РАЙОН» — она произносит границу вслух, а не оставляет читателю догадываться по размеру маркера. Слова «объект» и «адрес» в карточке не появляются. ТОЧКИ НЕТ — ДВА СЛУЧАЯ, оба дают карту с подсвеченным районом и без прочерка: координаты нет; координата есть, но лежит вне кадра. Второе не теоретическое — 2 132 строки из 34 021 (6.3%) в той же выборке имеют координату за пределами города, встречаются точки за сотни километров. Прижать такую к краю значило бы показать место, которого в данных нет; подпись в обоих случаях — «КООРДИНАТЫ НЕТ». Доступность: <svg> aria-hidden, всё его содержание (район) стоит рядом текстом дважды — в подписи карты и в мете сделки. Гейт витринных чисел: из скана исключён сгенерированный `ekb-districts.ts` — координаты полигонов не утверждают ничего о рынке. Исключение не бланковое: добавлена проверка, что исключённый файл несёт маркер генератора. Проверено глазами на dev-сервере с заглушкой бэкенда: три состояния (Кировский с точкой, Чкаловский — кадр едет, сделка без координаты). |
|||
| 7b12b07935 |
feat(mera/b2c): границы районов ЕКБ статикой — основа настоящей карты в игре
Карточка игры «Сыграйте против МЕРЫ» показывает декоративный прямоугольник: сетка и две «дороги», нарисованные CSS. Заменяем настоящей геометрией. ПОЧЕМУ СТАТИКА, А НЕ ТАЙЛЫ. Публичное дерево mera-public держится без единого внешнего запроса — шрифты self-hosted, Leaflet с unpkg сюда намеренно не тянули (шапка layout.tsx). Тайловый провайдер получал бы IP посетителя и то, что он смотрит, на странице, которая обещает «без звонков и регистрации». Границы районов не меняются годами, раздавать их в рантайме незачем. Что в коммите: · backend/scripts/export_ekb_districts_svg.py — генератор. У данных обязано быть проверяемое происхождение: источник — боевая gendesign_ekb_districts_geom (OSM через FDW, читается с 29.08 — грант #3227), допуск упрощения и проекция названы в докстринге вместе с причиной выбора. · _components/v3/ekb-districts.ts — сгенерированное, руками не править. 8 районов, 7826 символов путей, плюс project() для точки (lon,lat). Упрощение 0.0012° (~70-130 м) выбрано под масштаб карточки — она показывает район целиком, где такая ошибка меньше толщины линии. |
|||
| 2e82b0810f |
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
«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 дн»). |
|||
| b17a2a310c |
fix(mera/b2c): гейт витринных чисел ловит происхождение, а не форму записи
Ревьюер нашёл три дыры и воспроизвёл каждую на живом коде.
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, откуда ту же фразу уже сняли. Правка текста —
вне участка этой задачи (файлы вёрстки правит соседняя ветка).
|
|||
| 2c9b4ab547 |
feat(mera-public): витрина лэндинга v3 на замеренных числах
Литералы из дизайн-макета заменены величинами из /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 не тронуты. |
|||
| f612401202 |
feat(mera-public): серверный слой витринных чисел лэндинга
Загрузка /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. |
|||
| 5d93f13030 |
Merge pull request 'feat(mera/b2c): новый дизайн становится корнем meraocenka.ru, функционал старого перенесён' (#3236) from feat/b2c-v3-becomes-root into main
Some checks failed
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / perimeter-smoke (push) Blocked by required conditions
Deploy Trade-In / deploy-status (push) Blocked by required conditions
Deploy Infra Host / sync-infra-host (push) Successful in 7s
Deploy / changes (push) Successful in 8s
Deploy Trade-In / changes (push) Successful in 13s
Deploy / deploy-caddy (push) Has been skipped
Deploy / build-backend (push) Successful in 43s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy / build-worker (push) Successful in 44s
Deploy / build-frontend (push) Successful in 43s
Deploy / deploy (push) Successful in 1m36s
Deploy / deploy-status (push) Successful in 1s
Deploy Trade-In / build-frontend (push) Successful in 2m27s
Deploy / perimeter-smoke (push) Failing after 14s
Deploy Trade-In / test (push) Successful in 4m20s
Deploy Trade-In / build-backend (push) Has been cancelled
|
|||
| ac2633011b |
fix(mera/v2): форма принимала этаж больше этажности дома (#3226)
Some checks failed
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / test (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 2m15s
Deploy Trade-In / deploy (push) Successful in 1m3s
Deploy Trade-In / deploy-status (push) Successful in 2s
Deploy Trade-In / perimeter-smoke (push) Failing after 15s
|
|||
| 49652f4413 |
fix(mera/b2c): вернуть потерянные вопросы FAQ, починить адрес превью и честность двух комментариев
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
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 / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m0s
Правки по трём ревью ветки «v3 становится корнем».
FAQ. Из шести вопросов `content.ts` возражения v3 брали два, остальные
четыре (`how-accurate`, `vs-marketplace`, `personal-data`, `bank-report`)
не рендерились нигде: `Faq.tsx`, который выводил все шесть, удалён вместе
с лэндингом v1. Макетные возражения их тематически закрывали, но
пересказом — а оригиналы проходили юр-ревью. Содержательно пропали два
утверждения: «Имя, паспорт и документы на квартиру мы не спрашиваем» с
отсылкой к странице ПДн (152-ФЗ) и «показываем диапазон, а не одно число»
как прямой ответ про точность (рамка диапазона осталась только в
`AccuracyV3`, рядом с плейсхолдерными числами).
Теперь в секции один порядок чтения: все шесть вопросов из `FAQ` по id
плюс три макетных возражения, которым в `FAQ` пары нет («факт сделки»,
«почему так дёшево», «подписка»). Три макетных пересказа удалены в пользу
оригиналов; у «почему так дёшево» убран второй абзац — почти дословный
повтор второго абзаца `how-accurate`, который теперь стоит следующим.
Новых формулировок не добавлено. Отсылка к странице ПДн стала живой
ссылкой через `PublicLink` (обычный `<a>`, basePath не течёт); текст
ссылки — слова самого `content.ts`, найденные по вхождению.
Гейт стал двусторонним: тест перебирает `FAQ` и требует, чтобы КАЖДЫЙ id
был отрисован. Прежняя версия перечисляла два id руками и потому молчала
ровно в тот момент, когда четыре ответа исчезли со страницы. Проверено
мутацией: снятие вопроса, переименование id и переформулировка фразы про
ПДн красят тест по отдельности.
CADDY. `/trade-in/mera-public/v3` выбыл из @meraLongPages вместе со
страницей и никуда не попал — длинный адрес превью падал в catch-all 404,
хотя раньше вёл на страницу. Обе его формы добавлены в @meraV3Gone. Там же
редирект перестал терять query: было `redir * /`, стало `redir * {uri}`
со срезанием пути — как у @meraLongPages, где перенос UTM и обоснован.
Проверено на живом Caddy 2.11 по настоящему site-блоку: все четыре формы
дают 301 на `/` с сохранённой query, без параметров — чистый `/` без
хвоста `?`, соседние матчеры не задеты, лишний сегмент по-прежнему 404.
КОММЕНТАРИИ, ОПИСЫВАЮЩИЕ НЕСУЩЕСТВУЮЩЕЕ. Строка `page.tsx` в гейте
плейсхолдеров убрана: комментарий над ней утверждал, что устаревший путь
«перестал бы что-либо сканировать», но ни в корневом `page.tsx`, ни в
прежнем `v3/page.tsx` нет ни одного имени из `PLACEHOLDER_EXPORTS` —
страница только собирает секции. Замер подтверждает: множество найденных
плейсхолдеров со строкой и без неё совпадает, а сама по себе она не даёт
ничего. Держать её значило держать ложное ощущение охвата.
Дескриптор «Оценка вторичного жилья по рыночным данным · <регион>» на
внутренние страницы не возвращается — причина записана в докстринге
`InnerHeader`: эту шапку носят и `/estimate`, `/articles`, `/docs`, где
дескриптора не было никогда; требование юриста было про первый экран
лэндинга и там выполняется `HeroV3`; на самих юр-страницах то же сказано
сильнее — в тексте оферты (п. 1.3, 6.2) и в `FooterV3` на каждой странице.
|
|||
| 6976ca8915 |
feat(mera/b2c): лэндинг v3 становится корнем публичного домена
Вариант дизайна выбран — превью с /mera-public/v3 переехало в корень целиком, вместе с блоками, перенесёнными из v1 предыдущим шагом. Каталог v3/ и компоненты v1 (Hero / HowItWorks / WhatYouGet / DataSources / Faq / AddressForm / SiteHeader / SiteFooter) удалены: их данные живут в content.ts и уже читаются секциями v3, поэтому ушла только вёрстка. Юридические страницы (оферта, возврат, ПДн) держались на SiteHeader/ SiteFooter — единственные, кто их ещё импортировал. Переведены на InnerHeader + FooterV3, тем же способом, что /estimate, /articles и /docs. ПЕРИМЕТР. /v3 убран из @meraPages, @meraShortSlash и @meraLongPages, но не выброшен: адрес две недели был открыт наружу и разошёлся по закладкам, поэтому вместо тишины белого списка — 301 на корень (@meraV3Gone, обе формы). На это заведена проверка в public-perimeter.test.ts по образцу соседних; PREVIEW_ONLY стал пустым, а не удалённым — он держит проверку «наружу не открыто ничего сверх известных страниц» двусторонней. Из PUBLIC_SHORT_PATHS гварда /v3 убран: редирект отрабатывает в Caddy, до приложения запрос не доходит, а недостижимая строка в списке публичных путей сделала бы публичной будущую страницу с тем же адресом. Гейт Caddy→гвард односторонний и сам бы этого не поймал — обоснование в коде. NOINDEX ОСТАВЛЕН. Он держится за плейсхолдерные числа marketing-v3.ts (#2904), а не за то, какой вариант дизайна стоит в корне. Путь в гейте обновлён на page.tsx: placeholdersInUse() молча пропускает ненайденный файл, поэтому устаревшая строка не покраснела бы, а перестала бы сканировать. |
|||
| d4e106882c |
feat(mera/b2c): перенести потерянные блоки v1 в лэндинг v3
Корень meraocenka.ru отдаёт v1, новый дизайн живёт на /v3 и скоро станет корнем. Шесть вещей из v1 в v3 отсутствовали — при переключении они бы просто исчезли с публичного сайта: - реестра источников не было вовсе (SourcesV3, из SOURCE_GROUPS); - «что вы получите» и дисклеймера 135-ФЗ не было (WhatYouGetV3): о том, что это не отчёт аккредитованного оценщика, узнавал только тот, кто сам догадался открыть нужное возражение; - герой не называл границу покрытия (полное — Екатеринбург, частично — остальные города области, по другим регионам не считаем) и не говорил, что мы не покупаем квартиры и не берём их на продажу; - два вопроса FAQ («откуда вы знаете, сколько стоит именно моя квартира», «почему только Свердловская область») в возражениях v3 не звучали — теперь берутся из FAQ по id, а не копией строк; - карточка проверки не спрашивала город: гео-гейт #2576 на новом лэндинге был сломан (без города «Ленина, 1» уходит в одноимённую улицу ЕКБ); - она же спрашивала состояние обязательным полем и выбрасывала ответ на переходе — теперь он доезжает в черновике. Тексты перенесены дословно (проходили юр-ревью), новых чисел нет. Новые секции встали после «Точности»: сверяем прогноз с фактом → на каких данных → что получаете на руки, всё до первого разговора про деньги. Гейт: __tests__/v3-ported-copy.test.ts — проверен мутацией (снятие секции со страницы и переименование id вопроса красят его). |
|||
| 2d8edd2c4f |
feat(mera/b2c): раздел статей и хаб документов (макет 29.08) + числа статьи заменены на замер
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 58s
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
Последняя часть макета Макса «B2C модуль для МЕРА»: /articles, /articles/kak-ocenit-kvartiru, /docs. Проводка ссылок доведена до конца — «Статьи» и «Проверьте себя» в шапке и подвале перестали быть заглушками, EstimateHeader переименован в InnerHeader (он теперь стоит на трёх страницах, а не только на проверке). ЧИСЛА СТАТЬИ — НЕ ИЗ МАКЕТА. Макет утверждал «по нашим данным, средний разрыв между первой ценой в объявлении и ценой сделки — 4,1%, а у провисевших дольше трёх месяцев доходит до 9%». Прод-замер (poincare, 29.08) показал, что величины такого рода у нас нет вовсе: связки листинга со сделкой в данных не существует (cadastral_number пуст у всех 108 623 сделок и 111 693 объявлений). Заменено на то, что измеряется парно и честно: по 6202 объявлениям Домклик в ЕКБ (срез 18.07.2026) цену снижали 44%, а среди провисевших дольше трёх месяцев — 62% в медиане на 4,9%. Границы замера названы в самом тексте: это движение цены В ОБЪЯВЛЕНИИ, а не скидка на сделке, и объявлений моложе трёх недель в выборке нет (прогон обогащения их не захватил), поэтому по всему живому рынку доля ниже. Заодно сняты остальные непроверяемые утверждения: «расхождение с кадастровой до 40%» (кадастровой стоимости ПОМЕЩЕНИЙ нет ни в одной базе — проверить нечем), поправки §3 помечены как рыночная практика, а не наш замер, пороги фильтра §2 приведены к тем, что реально стоят в оценщике (радиус 1 км, площадь ±15%, минимум 5 аналогов + каскад послаблений). Плейсхолдеров marketing-v3 в статье не осталось. Периметр закрыт во всех трёх местах: @meraPages / @meraShortSlash / @meraLongPages в apps.caddy, PUBLIC_ROUTES и PUBLIC_SHORT_PATHS в гварде. @meraShortSlash до сих пор не проверялся ничем — добавлен гейт в public-perimeter.test.ts (фальсифицирован: снятие /docs из альтернации красит тест). Гейт #2904 теперь видит и статью (articles-content.ts в V3_SOURCES) — фальсифицировано отдельно: с одним этим файлом в списке снятие noindex красит гейт. Честные отличия от макета — в докстрингах страниц: масок реквизитов и макетной редакции оферты нет (источник правды — /oferta, /privacy, /refund от 13.08), регламент ответа поддержки «9:00-19:00» снят (никто его не устанавливал), фильтр рубрик и обещание «новые статьи каждую неделю» не рендерятся. Проверено: tsc, eslint, vitest 69/69, изоляция mera-public, скриншоты трёх страниц на 1180/375/320px без горизонтального переполнения. |
|||
| 51fd939f5c |
Merge pull request 'feat(mera/b2c): результат проверки в облике отчёта (макет 29.08 «МЕРА отчёт»)' (#3221) from feat/b2c-report-redesign into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / test (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 2m6s
Deploy Trade-In / deploy (push) Successful in 55s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
|
|||
| de66826b25 |
feat(mera/b2c): результат проверки в облике отчёта (макет 29.08 «МЕРА отчёт»)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
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 / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 59s
Редизайн результата /estimate по макету отчёта: шапка объекта (адрес + чипы параметров + «Изменить данные»), две большие бесплатные карточки, тизер платной части с shimmer-строками, честные состояния «мало данных» и «город не покрыт» в стилистике «ЧЕСТНЫЙ ОТВЕТ». Сознательные расхождения с макетом (задокументированы в шапке компонента): - «✓ расчёт готов и ждёт вас» не воспроизводится — платного расчёта для анонима не существует (анонимный /estimate закрыт, платёжного контура нет, #2896); тизер честно называется «что будет в полном отчёте», вместо кнопки оплаты — прежняя формулировка «приём оплаты подключается»; - бейджи МИР/СБП/VISA — за тем же TODO платёжного контура, что и в FooterV3; - email-подписка «сообщим когда добавим» — нет хранения и согласия (#2895), вместо неё живой канал поддержки; - «Расширить радиус до 3 км» — /coverage радиус не параметризует; - подписи бесплатных плиток — запиненные честные из coverage-copy. Возврат 150 ₽ — строкой из макета со ссылкой на /refund. Старый CoverageResult удалён (один рендер результата, не два). Фикстурное превью всех трёх тонов — /mera-public/ui-preview/report. Проверено: tsc чисто, lint чисто, vitest 27 passed, скрин трёх состояний. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| 9f45c7616c |
Merge pull request 'chore(ci): node 20 отслужил — рантайм фронтов и оба CI-джоба на node 24 LTS' (#3214) from chore/node24-runtime into main
Some checks failed
Deploy / changes (push) Successful in 8s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy Trade-In / changes (push) Successful in 12s
Deploy / deploy-caddy (push) Has been skipped
Deploy Trade-In / test (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 3m22s
Deploy Trade-In / deploy (push) Successful in 58s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy / build-frontend (push) Successful in 4m38s
Deploy Trade-In / perimeter-smoke (push) Successful in 14s
Deploy / deploy (push) Failing after 15m16s
Deploy / perimeter-smoke (push) Has been skipped
Deploy / deploy-status (push) Failing after 1s
|
|||
| a3e642fed4 |
Merge pull request 'chore(deps/mera): vitest 2 → 3 — заодно уходит вся dev-цепочка vite/esbuild' (#3216) from chore/vitest3-mera into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 13s
Deploy Trade-In / test (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 3m24s
Deploy Trade-In / deploy (push) Successful in 59s
Deploy Trade-In / deploy-status (push) Successful in 2s
Deploy Trade-In / perimeter-smoke (push) Successful in 14s
|
|||
| 2f91b4b34f |
Merge pull request 'feat(mera/b2c): лэндинг v3 доведён до макета 29.08 — лента, игра, два пути, возражения, статьи' (#3215) from feat/b2c-landing-v3-full into main
Some checks are pending
Deploy Trade-In / build-backend (push) Blocked by required conditions
Deploy Trade-In / build-frontend (push) Blocked by required conditions
Deploy Trade-In / build-browser (push) Blocked by required conditions
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / perimeter-smoke (push) Blocked by required conditions
Deploy Trade-In / deploy-status (push) Blocked by required conditions
Deploy Trade-In / changes (push) Successful in 13s
Deploy Trade-In / test (push) Has been skipped
|
|||
|
|
57ec24f364 |
chore(deps/mera): vitest 2 → 3 — заодно уходит вся dev-цепочка vite/esbuild
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 12s
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 1m18s
Мажор ради GHSA-5xrq-8626-4rwp (CRITICAL, vitest ≤2.x). Сам CRITICAL к нам неприменим — он про `--ui`, а `@vitest/ui` не установлен и скриптов с `--ui` нет — но держать заведомо непатченный рантайм тестов ради этого рассуждения не стоит: следующий, кто добавит `--ui`, про оговорку не узнает. Побочно закрылось больше, чем планировалось. vitest тянет vite сам, и диапазон у мажора другой: 2.1.9 → vite ^5.0.0 (резолв 5.4.21 → esbuild 0.21.5), 3.2.7 → vite ^5||^6||^7 (резолв 7.3.6 → esbuild 0.28.2). Так что одним мажором: vitest 2.1.9 → 3.2.7 GHSA-5xrq-8626-4rwp vite 5.4.21 → 7.3.6 GHSA-4w7w-66w2-5vf9, GHSA-fx2h-pf6j-xcff, GHSA-v6wh-96g9-6wx3 esbuild 0.21.5 → 0.28.2 GHSA-67mh-4wv8-2f99 OSV по локу: 4 → 1. Остаток — postcss 8.4.31, вендоренный ВНУТРИ next/node_modules; нашим локом не управляется вообще. @vitejs/plugin-react 4.3.4 → 5.2.0 — вынужденно и ровно поэтому: у 4.x peer на vite ^4||^5||^6, семёрка в него не попадает. У 5.2.0 — ^4||^5||^6||^7. В шестёрку не идём: там peer уже ^8.0.0. vitest.config.ts править не пришлось: в нём нет ничего из того, что мажор переименовал или убрал (ни environmentMatchGlobs, ни deps.inline, ни workspace) — только environment/globals/setupFiles/include, которые в 3.x как были. Приёмка (локально, node 26): npm run test 66 passed, 2 failed npm run type-check ok Те же самые 2 падения (LoginPage, ветки 429/401) дают и vitest 2 на этом же локе, и vitest 2 на локе ДО апдейта зависимостей — проверено прогоном в обеих конфигурациях. Это локальный node 26 против node 20/24 в CI, к мажору отношения не имеет. Гейт — CI. |
||
| be7d7d0205 |
feat(mera/b2c): лэндинг v3 доведён до макета 29.08 — лента, игра, два пути, возражения, статьи
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 12s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m18s
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
Макет «B2C модуль для МЕРА» (Макс, 29.08) — пять секций, которых превью /mera-public/v3 не имело (его шапка их и перечисляла как отсутствующие): - DealsTickerV3 — бегущая строка «ПРОГНОЗ → ФАКТ»; данные — ТЕ ЖЕ строки, что таблица сверок (PROOF_ROWS_PLACEHOLDER): один будущий контур — один набор реквизита, второй не заводится; - GuessGameV3 — игра «Угадай цену», три раунда со слайдером, сверка «ваш ответ / МЕРА / факт», итог с медианной ошибкой игрока; раунды — GAME_ROUNDS_PLACEHOLDER под гейтом #2904; - TwoPathsV3 — «сами / сделаем за вас»; заявка пути 2 ведёт в Telegram поддержки (формы и договора «под ключ» не существует); - ObjectionsV3 — FAQ «перед оплатой» на нативных details; ответы из выверенной честной копии (content.ts/oferta/refund), макетный ответ про «отметки продавцов» заменён тем, что в продукте есть (Росреестр + снятие с публикации); - ArticlesV3 — тизер «Разборы на данных»; карточки БЕЗ ссылок до появления раздела статей (следующий шард). Решение «модалка → /estimate» (StickyCtaV3) сохранено для всех CTA макета. Гейт noindex↔плейсхолдеры дополнен новыми файлами и GAME_ROUNDS_PLACEHOLDER. Проверено: tsc чисто, next lint чисто, vitest 27 passed, живой прогон игры на dev (раунд → сверка → следующая), полностраничный скрин 1:1 с макетом. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
|
|
4e8cf5ab62 |
chore(ci): node 20 отслужил — рантайм фронтов и оба CI-джоба на node 24 LTS
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI / changes (pull_request) Successful in 13s
CI Trade-In / browser-tests (pull_request) Successful in 1m16s
CI Trade-In / frontend-checks (pull_request) Successful in 1m30s
CI / frontend-tests (pull_request) Successful in 1m23s
CI / openapi-codegen-check (pull_request) Successful in 2m23s
CI Trade-In / backend-tests (pull_request) Successful in 5m47s
CI / backend-tests (pull_request) Successful in 18m12s
Node 20 вышел из поддержки 30.04.2026: security-патчи для него больше не выпускаются, а образ node:20-alpine продолжает собираться и молча уносить это в прод. Node 24 — текущая Active LTS. Меняется ровно major рантайма, больше ничего: frontend/Dockerfile node:20-alpine → node:24-alpine (deps/builder/runner) tradein-mvp/frontend/Dockerfile то же, три стадии .forgejo/workflows/ci.yml node-version "20" → "24" (два джоба) .forgejo/workflows/ci-tradein.yml то же (один джоб) Версия в CI намеренно держится равной major'у из Dockerfile — так было и раньше, комментарии рядом обновлены вместе с числом, чтобы не разошлись. Ни `engines`, ни `.nvmrc` в проекте нет — других мест, где закреплён major, не осталось (проверено grep'ом по Dockerfile/yml/md). Совместимость: next 15.5.24 поддерживает node 20/22/24; sharp 0.35.4 — node ^18.17 || ^20.3 || >=22, prebuild linuxmusl-x64 есть. Приёмка — этот самый CI: джобы фронтов теперь выполняются на node 24, так что зелёный прогон PR и есть доказательство. Локально проверить нечем — на машине node 26, это не тот major. |
||
|
|
b52ec68aad |
fix(deps/mera): 6 уязвимых пакетов во фронт-локе — пересборка без правки package.json
All checks were successful
CI Trade-In / changes (pull_request) Successful in 14s
CI / changes (pull_request) Successful in 15s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (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
`npm update --package-lock-only` в tradein-mvp/frontend: package.json не тронут,
все диапазоны прежние — обновился только резолв внутри них.
OSV по локу: 6 уязвимых версий → 4. Закрыто:
sharp 0.34.5 → 0.35.4 GHSA-f88m-g3jw-g9cj
nanoid 3.3.17 → 3.3.18 GHSA-2v37-7h3g-55p8
Заодно (без CVE, в тех же диапазонах): next/@next/* 15.5.23 → 15.5.24,
js-yaml 4.3.1 → 4.3.2, @tanstack/react-query 5.101.4 → 5.102.8,
@typescript-eslint/* 8.66.0 → 8.68.0, rollup 4.62.4 → 4.63.1,
@testing-library/react 16.3.2 → 16.3.3, ws 8.21.2 → 8.21.3.
Остаток (4) диапазонами не чинится, вынесен в отдельную задачу:
postcss 8.4.31 — вендорится ВНУТРИ next/node_modules, нашим локом не управляется;
vitest 2.1.9 → vite 5.4.21 → esbuild 0.21.5 — dev-цепочка, лечится мажором
vitest 2 → 3.2.6+. GHSA-5xrq-8626-4rwp (CRITICAL) при этом неприменим:
@vitest/ui не установлен, скриптов с --ui нет.
Приёмка (локально, node 26):
npm ci --legacy-peer-deps — 566 пакетов, ok
npm run build — ok, весь роут-набор собрался
npm run test — 66 passed, 2 failed (LoginPage 429/401)
Два падения НЕ от апдейта: те же 2 теста падают ровно так же на СТАРОМ локе
(проверено откатом лока + npm ci + прогоном того же файла). Локальный node 26
против node 20 в CI; гейт — CI.
Хойстинг в диффе выглядит как даунгрейд, но им не является: picomatch 2.3.2
переехал из micromatch/node_modules/ наверх, а 4.0.5 → 4.0.7 ушёл под
tinyglobby/ вместе с fdir 6.5.0. Реального понижения версий нет.
NB: npm 12 локально требует --allow-remote=all даже при --package-lock-only
(ничего не ставится, только резолв). В CI npm 10 — там ограничения нет.
|
||
|
|
1dcddec9e1 |
fix(mera/v2): плашка версии лежала на главной кнопке и съедала клик
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 12s
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 57s
Замер на проде 27.08 (окно 1512×900): плашка стояла в левом нижнем углу (`left:16; bottom:16`), а туда же приходит липкая кнопка «ОЦЕНИТЬ КВАРТИРУ» из панели параметров. Прямоугольники: кнопка 59…484 по X и 859…904 по Y, плашка 16…284 и 859…884 — перекрытие по всей высоте плашки. Следствий два, и второе хуже первого. Надпись на кнопке читалась разорванной. И `document.elementFromPoint` в центре кнопки возвращал плашку: клик по левой трети главного действия продукта не доходил до кнопки вообще. Плашка переезжает в правый нижний угол, НАД кнопку поддержки (44px от низа, z-index 25). Там свободно: ниже только пассивная «Сводка объекта» без органов управления — перекрыть её краем ничего не стоит. Вдобавок плашка становится сквозной для указателя, а ссылка «История версий» внутри — нет. Это страхует от повторения: что бы под плашкой ни оказалось в будущем, клик достанется ему, а не ей. Три теста закрепляют оба следствия разбора: якорь не левый, указатель проходит насквозь, ссылка жива. Прогон: 68 тестов зелёные, tsc чист. |
||
|
|
e85cafd9a9 |
fix(mera/b2c): подсказка дублировала адрес, а числа в результате спорили
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
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 1m0s
Оба дефекта найдены проходом клиентского пути на проде 27.08.
ПОДСКАЗКА АДРЕСА печатала одно и то же дважды. Строка списка состоит из
главной строки (`label`) и пояснительной (`full_address`), но подсказчик
для дома отдаёт их совпадающими буква в букву — проверено на проде:
{"label":"Свердловская область, г. Екатеринбург, ул. Малышева, д. 51",
"full_address":"Свердловская область, г. Екатеринбург, ул. Малышева, д. 51"}
Человек видел адрес продублированным в каждой строке выпадающего списка.
Показываем вторую строку только когда она отличается: замысел (короткое имя
сверху, полный адрес под ним) сохраняется, если подсказчик когда-нибудь
начнёт их различать.
ЧИСЛА В РЕЗУЛЬТАТЕ спорили друг с другом. Плитка сверху: «30 похожих
квартир продаётся в радиусе 1 км». Плитка под ней: «столько в среднем висит
объявление из этих 6». Слово «этих» указывает на 30, а стоит рядом 6 —
читается как ошибка в расчёте.
Шесть — это те объявления, у которых известна дата публикации; медиана
считается только по ним. Теперь доля названа явно («дата известна у 6 из
30»), а когда известна у всех — не упоминается вовсе, чтобы «30 из 30» не
шумело. Честное имя величины сохранено: это возраст активного объявления,
а не срок продажи (тест на это как был, так и остался).
Проверено: 27 тестов mera-public зелёные, tsc --noEmit чист.
|
||
|
|
0231338a7f |
fix(mera/ui): карта перестала перекрывать интерфейс, страница помещается в окно
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
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 / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m6s
Три правки, две причины — обе найдены прогоном по живому проду 27.08 под аккаунтом админа. ## Карта поверх всего (два симптома, один корень) Контейнер мини-карты в HeroBar имел position: relative БЕЗ z-index. Такой блок контекст наложения не создаёт, поэтому внутренние слои Leaflet (тайлы 200, оверлеи 400, маркер 600, атрибуция 800) и карточка адреса (750) конкурировали не между собой, а со всей страницей. Отсюда: • меню аккаунта (UserMenu, z-index 200) открывалось ПОД картой; • карточка «АДРЕС» всплывала поверх таблицы раздела «Продажи в доме». Лечится изоляцией контейнера (isolation: isolate), а не гонкой чисел: перебивать 800 у атрибуции пришлось бы в каждом новом элементе, и гонка возвращалась бы. Меню шапки заодно поднято до 1000 — оно обязано быть сверху по замыслу, а не по совпадению. ## Страница не помещалась в окно Масштаб артборда 1536×1024 считался от window.innerWidth, который ВКЛЮЧАЕТ вертикальную полосу прокрутки. На рабочей области 1425px артборд выходил 1440px — страница получала постоянный горизонтальный скролл. Воспроизводится на любом десктопе: страница длинная, вертикальная полоса есть всегда. Считаем от document.documentElement.clientWidth. Пороги isMobile/ isSmallViewport намеренно оставлены на innerWidth: это классификация устройства, а не расчёт геометрии. |
||
| a4604dd9db |
fix(mera/ui): карточка прогресса перестала выдумывать стадии (#3081, вариант A)
All checks were successful
CI / changes (pull_request) Successful in 11s
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 59s
Запрос оценки один и блокирующий (POST /estimate, mutation.isPending) — промежуточных событий по источникам не существует, а карточка рисовала пофайловые «сбор...» с полосками 60%, счётчик 0/5 и подпись про Celery group с таймаутом. Пользователь не мог отличить «думает» от «завис», а серверная механика текла в UI-текст. Вариант A из задачи (только фронт): - на pending строки нейтральны («ожидает ответ»), раскраска — только по факту estimate.sources_used после ответа; - общая полоса на pending — честная неопределённая анимация вместо выдуманного процента (Math.min(95, ...) убран); - счётчик N/M на pending заменён на «опрашиваем…» (0/5 при идущей работе читался как отказ); - подпись без Celery/таймаута, тон legacy-заглушки «Считаем оценку…»; - недостижимые ветки error/loading у строк удалены (их ничто не выставляло). Вариант B (реальный статус с бэкенда) отклонён сознательно: /estimate остаётся синхронным по решениям #3082/#3083, городить async+task_id ради прогресс-бара — против них. Preview-страница /ui-preview/estimate теперь рендерит ОБА состояния карточки. Проверено скриншотом на dev (pending + done). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| 60cb510ef2 |
refactor(caddy): разделить site-блоки по хостам под переезд (#3062)
Some checks failed
Deploy / changes (push) Successful in 8s
Deploy Trade-In / changes (push) Successful in 12s
Deploy / deploy-caddy (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy / build-worker (push) Successful in 48s
Deploy / build-backend (push) Successful in 49s
Deploy / build-frontend (push) Successful in 49s
Deploy / deploy (push) Successful in 1m37s
Deploy / deploy-status (push) Successful in 1s
Deploy Trade-In / build-frontend (push) Successful in 2m34s
Deploy / perimeter-smoke (push) Successful in 10s
Deploy Trade-In / test (push) Failing after 3m56s
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / deploy (push) Has been skipped
Deploy Trade-In / perimeter-smoke (push) Has been skipped
Deploy Trade-In / deploy-status (push) Failing after 1s
|
|||
| 4ae14055ec |
feat(tradein/v2): позиция квартиры на рынке — плашка на карточке рекомендованной цены (#2899) (#3024)
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / test (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 2m33s
Deploy Trade-In / deploy (push) Successful in 1m46s
Deploy Trade-In / deploy-status (push) Successful in 2s
Deploy Trade-In / perimeter-smoke (push) Successful in 18s
|
|||
| 8db357ef30 |
test(tradein): гейт «снять noindex можно только вместе с плейсхолдерами» (#2904) (#2948)
All checks were successful
Deploy Trade-In / changes (push) Successful in 14s
Deploy Trade-In / test (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 3m18s
Deploy Trade-In / deploy (push) Successful in 1m6s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
|
|||
|
|
73c4c487ed |
fix(mera/b2c): житель Серова получал «вы вне области», а подсказки игнорировали выбранный город
All checks were successful
CI / changes (pull_request) Successful in 10s
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 1m6s
CI Trade-In / backend-tests (pull_request) Successful in 4m50s
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Successful in 48s
Два дефекта, найденных прогоном сценария глазами посетителя на живом домене. ## 1. Город предлагали выбрать, но отвечать по нему не умели Дропдаун на сайте (`OBLAST_CITIES`, city-registry.ts) и списки покрытия (`COVERAGE_GREEN/YELLOW_CITIES`, trade_in.py) — одно множество, записанное в двух местах. Они разошлись в обе стороны: предлагали, но не отвечали: Серов отвечали, но не предлагали: Берёзовский, Среднеуральск, Ревда Житель Серова выбирал СВОЙ город из НАШЕГО дропдауна и получал: «Этот адрес вне области, по которой мы собираем данные. Сейчас это Свердловская область: Екатеринбург целиком и ещё несколько городов вокруг.» Про город в той же самой области. Серов при этом покрыт данными: 363 активных объявления в радиусе 15 км, все свежие (замер по проде). Поэтому добавлен в жёлтый тир, а не убран из дропдаунa; три недостающих города добавлены на фронт. Шапка city-registry.ts этот риск прямо предсказывала — «перед добавлением 7-го города сверить оба списка вручную, теста на это пока нет». Теперь тест есть: бэкендовый сьют читает TS-реестр и требует РАВЕНСТВА множеств. Плюс проверка, что у каждого города с порогом есть центроид, — иначе порог мёртвый, город по координатам не резолвится. ## 2. Подсказки не слушались выбранного города `city_hint` доезжает до геокодера, но на выдачу не влияет: его смотрит только екатеринбургский кадастровый тир (как признак «речь не про ЕКБ, тир пропускаем»), а DaData-тир ограничен регионом целиком и хинта не принимает. Замер: выбран Серов, введено «Ленина 1» → первой подсказкой «Невьянский р-н, пгт Верх-Нейвинский». Человек выбирает верхний вариант и считает чужой дом — ровно баг #2576, ради которого город и спрашивают. Публичная ручка теперь подставляет город в саму строку запроса. Проверено на проде: «Серов Ленина 1» даёт серовскую выдачу целиком. Для Екатеринбурга подстановка безвредна — три разных адреса дали тот же результат с префиксом и без, поэтому правило одно на все города, без исключения для основного трафика. Чинится в публичной ручке, а не в геокодере: там от `city_hint` зависит поведение закрытого контура (`target_city_ambiguous`). ## Фикстура теста `_FAR_AWAY_CITY` стояла в 21 км от центра Серова и работала как «далеко от всех» лишь потому, что Серов не был поддержан. Переехала в Тавду — 271 км до ближайшего центроида. ## Мутации убрать Серов из покрытия (состояние прода) → падает сверка списков не подставлять город в строку → падает проверка ручки откат → 21 passed Плюс backend 75 passed, vitest 56 passed, tsc, lint, build, isolation guard. `city-registry.ts` добавлен в paths-фильтр БЭКЕНДОВОГО лэйна: сверку списков делает бэкендовый тест, и без этой строки правка одного лишь дропдауна её бы не запускала — то есть ровно тот путь, которым списки и разошлись. |
||
|
|
e74e9106b3 |
fix(mera/b2c): /estimate снова закрывался гвардом — короткий адрес завели в Caddy, но не в RouteGuard
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
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 / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 55s
Замер на проде сразу после выкатки #2913: страница `/estimate` отдаёт 200 и выглядит правильно, но после гидратации монтируется `GuardedRoute` — 404 /trade-in/api/v1/me ×3 404 /trade-in/api/v1/trade-in/support/anon/unread плюс виджет чата поддержки из закрытого контура и экран «Не удалось проверить доступ» внизу страницы. Форма при этом переставала работать ВОВСЕ: разметка с сервера видна, поле принимает текст, а обработчики не подключены — человек печатает адрес и не получает ни подсказок, ни ошибки. Проверено, что дело не в API: ручной `fetch` из той же страницы отдаёт 200 и восемь подсказок. Причина ровно та же, что чинил PR #2910 для `/`: список публичных путей в гварде и матчер `@meraPages` в Caddyfile — одно множество, записанное в двух местах. Тогда разошлись на `/`, теперь на `/estimate`. Оба раза страница отдавала 200, ломалась только в браузере, и ни один тест этого не видел. Комментарий в шапке гварда инцидент не предотвратил (он там был), поэтому совпадение теперь запинено ТЕСТОМ: он читает Caddyfile и требует, чтобы каждый путь из `@meraPages` проходил `isPublicPath`. Сверяемся с источником, а не дублируем список третий раз. Мутация — вернуть список к состоянию, которое уехало на прод: 2 failed / 3 passed × пропускает короткие адреса публичного домена × совпадает с тем, что публичный домен реально раздаёт (Caddyfile) откат → 5 passed. Плюс vitest 56 passed, tsc, lint, isolation guard 41 файл. `Caddyfile` уже в paths-фильтре фронтового лэйна (#2913), так что правка одного лишь конфига теперь тоже гоняет этот гейт. |
||
|
|
56194c606a |
fix(mera/b2c): семь дефектов публичного периметра, найденных состязательным ревью
Some checks failed
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Failing after 8s
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 / browser-tests (pull_request) Successful in 46s
CI Trade-In / frontend-checks (pull_request) Successful in 1m3s
CI Trade-In / backend-tests (pull_request) Successful in 4m42s
Ревью четырьмя независимыми линзами (периметр, семантика Caddy, политика ПДн
против кода, фронт) + по два проверяющих на каждую находку. Ниже — то, что
пережило проверку и воспроизведено на живом коде, а не выведено из чтения.
## Caddy: открытый редирект и потерянные ссылки
Захват хвоста регекспом (`^/trade-in/mera-public/(.+)$` → `redir /{re…1}`) —
открытый редирект. Захват берётся из РАСКОДИРОВАННОГО пути, поэтому
`/trade-in/mera-public/%5Cevil.example/pay` даёт цель `/\evil.example/pay`, а
браузеры трактуют `/\` как `//` — Location уводит на чужой хост. Готовая
фишинговая заготовка с домена, который напечатан внутри оферты и уходит
модератору эквайера. Заменено поимённым списком путей: такой адрес просто не
матчится.
Адреса со слэшем на конце (`/oferta/`, и длинные `…/oferta/`) отдавали 404 —
ровно те ссылки, ради сохранности которых редирект и делался. Добавлена
нормализация, цепочка замкнута (проверено: 2 перехода → 200).
Query-строка терялась: размещённые ссылки с UTM приходили бы в аналитику как
прямой заход. `uri strip_prefix` + `{uri}` переносит её. Обёртка `route`
обязательна — без неё `redir` выполняется раньше `uri` и Location равен
исходному адресу (бесконечный цикл, поймано на стенде).
`/v3` — черновое превью с маркетинговыми плейсхолдерами — было открыто на
боевом домене молча. Теперь названо вслух и запинено тестом.
## Гейты, которых не было
`caddy validate` не звал НИ ОДИН workflow, а deploy применяет конфиг не через
`reload` (тот отказался бы принять битый), а через `up -d --force-recreate` —
опечатка уводит контейнер в crash-loop и роняет ВСЕ домены. Добавлен гейт в
ci.yml, тем же образом caddy:2, что и на проде.
Проверка «роут ↔ Caddy» была односторонней и пропускала обратную ошибку —
путь, открытый наружу, о котором приложение не знает. Так и уехал `/v3`.
Теперь двусторонняя, плюс проверка, что для каждой страницы есть 301.
## Бюджет внешнего геокодера
Per-IP окна ограничивают одного клиента, но не сумму: 40/мин с адреса — это
57 600 в сутки при бесплатном тире DaData в 10 000, ОБЩЕМ с закрытым контуром.
Подтверждено на проде: достаточно упомянуть не-екатеринбургский город, чтобы
локальный тир отключился и запрос гарантированно ушёл во внешний сервис. То
есть один скрипт оставлял без подсказок платящих пилотов.
Per-IP снижен до 20/мин, добавлен общий суточный потолок 2000 и потолок
одновременных подсказок (4): кадастровый тир уходит в FDW-скан чужой базы,
держит соединение около секунды, а пул общий с B2B — полтора десятка
параллельных публичных запросов клали бы закрытый контур.
## «Адрес нигде не сохраняется» — теперь правда целиком
Две утечки, обе воспроизведены:
1. ЖУРНАЛЫ. Геокодер печатает введённую строку открытым текстом на каждый
вызов, прод пишет stdout в persistent journald — адрес ложился на диск
рядом с IP того же запроса в access-логе Caddy. Закрыто фильтром логов на
время публичного запроса (contextvar, переживает await и to_thread).
Закрытый контур логи сохраняет: они нужны для разбора жалоб пилотов.
2. МОНИТОРИНГ. sentry_sdk кладёт в событие ПОЛНОЕ тело запроса — а тело
публичной ручки это ровно `{"q": "<адрес>"}`; `send_default_pii=False` тут
не гейт, он про куки. Плюс брэдкрамб httpx несёт адрес в query геокодера.
Закрыто `scrub_public_address`.
Текст п. 5.4 политики расширен до «ни в журналы веб-сервера, ни в технические
журналы, ни в мониторинг» — ровно то, что теперь обеспечено кодом.
## Фронт
- Отмена запроса подсказок откладывалась внутрь следующего debounce-такта и
не наступала вовсе, если человек переставал печатать: ответ по старой строке
долетал и ложился в список. Контроллер создаётся сразу, отменяется в cleanup.
- Список схлопывался на каждое нажатие — клик по намеченному пункту
промахивался. Старая выдача висит, пока не пришла новая.
- «Комнат» с лэндинга — свободный текст: «студия» не совпадала ни с одним
option, селект показывал пустоту, parseInt давал NaN, на сервер уходил
rooms: null → 422 с текстом «сломалось на нашей стороне». Нормализация
вынесена чистой функцией и покрыта тестами.
- У пробы покрытия не было ни таймаута, ни отмены: оборванное соединение
оставляло кнопку в «Смотрим данные…» навсегда. 15 с + понятный текст.
- Ошибка подсказок глушилась в пустой список — тупик без объяснения.
- Комбобокс: Tab проваливался в кнопки подсказок, список не закрывался по
уходу фокуса и перекрывал поля, Escape оставлял висячий aria-activedescendant.
## Проверено
Локальный стенд (реальный site-блок Caddy + заглушка): 18 маршрутов, включая
`%5C`, `//`, `%2F` — все три теперь 404. vitest 55 passed, backend 17 passed по
публичному API, tsc, lint, build, isolation guard 41 файл, caddy validate.
Мутации: снять редакцию логов → падает тест журналов; не вырезать тело запроса
→ падает тест мониторинга; убрать /estimate из Caddy → падает тест маршрутов.
|
||
|
|
7424c283d5 |
feat(mera/b2c): отдельный экран оценки на meraocenka.ru и короткие адреса
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 8s
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 / browser-tests (pull_request) Successful in 44s
CI Trade-In / frontend-checks (pull_request) Successful in 1m2s
CI Trade-In / backend-tests (pull_request) Successful in 4m41s
## Экран проверки — /estimate Проверка квартиры вынесена на собственный адрес: там у автокомплита есть место под список подсказок, а у результата — место рядом с полями. Форма в герое лэндинга осталась входной точкой и уводит сюда, донося набранное через sessionStorage (НЕ через query — адрес в URL попал бы в access-лог Caddy рядом с IP посетителя, а мы на той же странице обещаем ничего не хранить). Показывает живую пробу покрытия: сколько похожих квартир продаётся рядом и сколько в среднем висят их объявления. Ни одной рублёвой цифры — цену продаёт платный шаг. Тексты вердикта вынесены чистой функцией (coverage-copy.ts) и покрыты тестами: подпись под возрастом обязана говорить «объявление», а не «продаётся» (выборка цензурирована), при неизвестном возрасте плитки нет вообще, а пустая когорта объясняется как факт о рынке с подсказкой, что поменять, — директива «никогда не блокировать вывод». ## Короткие адреса Человек больше не видит /trade-in/mera-public/... — только /, /estimate, /oferta, /refund, /privacy. Длинные адреса отдают 301 на короткие: у страницы один канонический адрес, старые ссылки живы. Цена решения: ссылки работают только на meraocenka.ru (короткие пути раздаёт этот хост). Открывать лэндинг для проверки нужно там же, а не с gendsgn.ru. Ссылки эмитятся обычным <a> (PublicLink) — next/link подставляет basePath, и href="/estimate" уехал бы на несуществующий /trade-in/estimate. ## Три дефекта, найденных на живом сайте 1. Палитра v3 никуда не доезжала. b2c-tokens.ts не импортировал НИКТО, ни одна --b2c-* переменная не объявлялась, каскад молча пропускал такие декларации — лэндинг отдавал 200 бесцветным. Добавлен мост b2cVars, гейтом стал тест: каждая использованная в CSS переменная обязана быть объявлена. 2. Голый /trade-in/mera-public падал в 404 — матчер был со слэшем и звёздочкой. Ровно туда вела «Главная» в подвале. 3. «Для бизнеса» вела на «/» — то есть на сам лэндинг. Теперь абсолютный адрес B2B-контура. Пункты «Проверьте себя» и «Продажа под ключ» вели на якоря, которых нет нигде: приведены к виду «Статьи» — видны, но не кликабельны. ## Периметр и приватность Подсказки переведены на POST: access-лог публичного домена пишет URI целиком, то есть GET с ?q= сохранял бы адрес квартиры в файл. Тело в лог не попадает. Метод запинен тестом — это часть обещания, а не стиль. П. 5.4 политики ПДн переписан ВМЕСТЕ с кодом: прежний текст утверждал, что адрес не покидает браузер, и это перестало быть правдой. Новый говорит точно — передаётся, используется однократно, в базах не сохраняется. Последнее проверено по коду: suggest() работает без кэша, проба — один SELECT, аудит пишет строку только при наличии username. PUBLIC_ESTIMATE_ENABLED сузился до платного шага (бесплатная проба не хранит ничего, платный расчёт хранит). ## Проверено vitest 47 passed (9 файлов), tsc, next lint, next build, isolation guard 40 файлов, backend 75 passed, caddy validate = Valid configuration. Мутации структурных гейтов: убрать --b2c-accent-text из b2cVars → падает тест палитры убрать /estimate из @meraPages → падает тест маршрутов добавить импорт next/link → падает тест basePath откат → 14 passed Caddyfile добавлен в paths-filter фронтового лэйна: его читает тест маршрутов, и без этой строки правка одного лишь Caddyfile не запускала бы ни один гейт. Refs #2894, #2895 |
||
|
|
6348f51ff4 |
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 Trade-In / frontend-checks (pull_request) Successful in 54s
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
На meraocenka.ru посетитель видел внизу страницы экран проверки доступа из
закрытого контура. Замер на проде: три вызова GET /trade-in/api/v1/me → 404
плюс подтянутый следом клиент чата поддержки.
Причина. Caddy на коротком домене переписывает путь ВНУТРЕННЕ (rewrite, не
redir): посетитель остаётся на meraocenka.ru/, а Next получает
/trade-in/mera-public. Поэтому SSR видит /mera-public и bypass срабатывает, а
после гидратации usePathname() возвращает «/» — путь из адресной строки — и
bypass отваливается. Монтируется GuardedRoute, летит запрос к /me, а на этом
хосте Caddy намеренно не проксирует /trade-in/api/* → 404 → NoAccessScreen.
В шапке RouteGuard прямо стояло допущение «домен/rewrite настраиваются
отдельным PR — на поведение гварда это не влияет, он смотрит на путь». Оно
сломалось ровно в тот момент, когда домен появился; заменено разбором факта.
Правка: четыре коротких адреса публичного домена (/, /oferta, /refund,
/privacy) добавлены в bypass ТОЧНЫМ совпадением, отдельной веткой. Префиксную
проверку для «/» применять нельзя — startsWith("/") истинно для любого пути,
то есть один такой элемент сделал бы публичным всё приложение. Эти адреса
существуют только на публичном домене: на gendsgn.ru их ловит catch-all 404, а
«/» отдаёт server-side redirect на /v2 из next.config, то есть приложение по
этому пути не рендерится вовсе.
Тест на оба инварианта: короткие адреса публичны И закрытый контур (/v2,
/admin/*, /history, /scrapers/*, /login) — нет. Мутация «точное совпадение →
префиксное» роняет два теста из четырёх, проверено прогоном.
Проверено: vitest 33 passed (7 файлов), tsc --noEmit, isolation guard 31 файл.
|