Commit graph

3189 commits

Author SHA1 Message Date
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>
2026-08-30 00:07:07 +05:00
ba35c68eb2 Merge pull request 'feat(mera/b2c): настоящая карта улицы в карточке игры + мобильный лэндинг перестал съедать 45% экрана' (#3245) from feat/b2c-game-real-map into main
All checks were successful
Deploy / changes (push) Successful in 9s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy Trade-In / changes (push) Successful in 13s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy / build-backend (push) Successful in 42s
Deploy / build-worker (push) Successful in 43s
Deploy / deploy (push) Successful in 1m9s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Successful in 2m10s
Deploy Trade-In / test (push) Successful in 4m11s
Deploy Trade-In / build-backend (push) Successful in 1m19s
Deploy Trade-In / deploy (push) Successful in 1m55s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
2026-08-29 18:45:22 +00:00
c6fc21fff4 Merge pull request 'perf(tradein/browser): якорная вкладка выдачи вместо переоткрытия origin на каждую карточку' (#3244) from perf/domclick-anchor-serp-tab into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 14s
Deploy Trade-In / test (push) Has been skipped
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / build-browser (push) Successful in 2m12s
Deploy Trade-In / deploy (push) Successful in 1m32s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
2026-08-29 18:29:41 +00:00
bot-backend
2c29151e85 docs(tradein/browser): origin работает через куки контекста, а не через Referer
All checks were successful
CI Trade-In / changes (pull_request) Successful in 14s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 15s
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
CI Trade-In / browser-tests (pull_request) Successful in 1m37s
Оба комментария про origin обещали «органическую навигацию с реальными
cookies/Referer». Referer тут не при чём: playwright'овский goto() этот
заголовок не шлёт вовсе, так что органической навигацией заход на origin не был
никогда. Работает он через куки контекста — пропуск QRATOR, выданный на выдаче,
остаётся в контексте и годится для карточки.

Различие не косметическое: из «нужен Referer» следует, что origin надо
переоткрывать перед каждой карточкой, а из «нужны куки» — что достаточно одного
раза. Второе и делает якорная вкладка предыдущего коммита; заодно дописано,
почему при reuse_context повторный заход на выдачу не нужен.
2026-08-29 21:27:12 +03:00
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 хрома), то есть возвращать её
там значило бы вернуть тот же дефект планшету.
2026-08-29 23:25:44 +05:00
2390eec740 fix(mera/b2c): вернуть координаты сделки, снесённые моим же коммитом про студию
Коммит 555cce44 обещал в сообщении одно — подписать студию вместо «0-к», — а
сделал ещё и другое: удалил миграцию 280_landing_showcase_deals_coords.sql и
откатил правки mera.py, landing_showcase_deals.py и двух тестов из коммита
cd2a7671. Заметил не я: об этом написал агент, которому потом достался номер
281 и который увидел дыру на 280.

ПРИЧИНА. `git commit` фиксирует ИНДЕКС ЦЕЛИКОМ, а не то, что было добавлено
последним `git add`. В индексе главного рабочего дерева лежали staged-удаления,
оставшиеся от параллельных агентов (они работают в своих worktree, но индекс
основного дерева переживает переключения веток). Я добавил два файла, а
закоммитил вместе с ними чужие удаления.

Что восстановлено: миграция 280 целиком, поля lat/lon в ShowcaseDeal, перенос
координат в пересчёте витрины, оба теста.

Обе величины нужны и не заменяют друг друга: схема улицы (281) закрывает 92%
сделок, координата (280) — карту района для остальных 8%. Конфликты сведены
вручную: механическое «оставить обе стороны» задвоило список колонок в INSERT
и в SELECT, что тесты бы пропустили, а прод — нет.

Проверено: 5085 passed, 35 skipped; миграции 275-281 без дыры; список колонок
INSERT сверен со списком значений программно (15 и 15, порядок совпадает).
2026-08-29 23:25:44 +05:00
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: улица сматчилась; не
сматчилась (виден район); полей схемы нет вовсе (тот же район, без падения).
2026-08-29 23:25:44 +05:00
ca3f073b0e feat(mera/b2c): улица сделки доезжает до витрины готовой схемой, а не геометрией
Карта в карточке игры показывала полигон района — единственную геометрию, до
которой у tradein был доступ. Улицы живут в базе gendesign, foreign table и
гранта на них не было.

Мост по образцу соседей (v_tradein_cad_buildings / v_tradein_osm_poi_ekb):
gendesign 195 — вьюха v_tradein_osm_roads_ekb (highway + water) с GRANT В ТОМ
ЖЕ ФАЙЛЕ (после #3227 грант отдельной миграцией теряется при пересоздании);
tradein 281 — foreign table gendesign_osm_roads_ekb плюс колонки street_name /
street_scheme в landing_showcase_deals.

Пересчёт витрины кладёт в строку УЖЕ СПРОЕЦИРОВАННЫЕ SVG-пути окна 840x840 м
вокруг центра улицы. Проекция — та же равнопромежуточная с cos(широты), что в
export_ekb_districts_svg.py; второй в проекте нет. Замер 2026-08-29: GeoJSON
того же окна 7-8 КБ на строку, схема — 2.8-2.9 КБ.

ЧТО ДАННЫЕ ВЫДЕРЖИВАЮТ, И НИ СЛОВОМ БОЛЬШЕ
  · Это УЛИЦА, а не дом: deals.address уровня улицы, номер дома у 2.7% сделок.
    В схеме намеренно НЕТ координат окна и констант проекции — точку дома по
    ней нельзя поставить даже случайно. Это замок, а не забывчивость.
  · Зданий нет: cad_buildings — 18 307 контуров на город, в плотном центре 51
    здание на радиус 450 м, где их в разы больше. Нарисованная застройка
    заявляла бы полноту, которой в данных нет.
  · Улицы — фильтрованная выгрузка «источников шума»: именованные покрыты
    хорошо, дворовые и служебные проезды отсутствуют.
  · Улица сматчилась у 550 названий из 654 — 31 410 сделок из 34 021 (92.3%).
    Остальным street_scheme = NULL, и это штатно: фронт показывает район,
    который для этого и оставлен.
  · Перекрёсток («Челюскинцев/Шейнкмана») берём первой улицей: таких адресов
    три на 34 021 сделку, и обе улицы одинаково верны на уровне улицы.
  · Наличие схемы НА ОТБОР СТРОК НЕ ВЛИЯЕТ — то же правило, что запрещает
    отбор по величине ошибки: иначе витрина показывала бы не работу оценщика,
    а те 92% адресов, что удобно легли на OSM.

Тесты (каждый сломан вручную и покраснел): нормализация на реальных адресах
включая ё/е и «8 Марта»; недоступная вьюха и упавший запрос дают None, а не
исключение; схема не раздувается — потолок в байтах на реальной плотности плюс
прямая проверка округления до 0.1.
2026-08-29 23:25:44 +05:00
4e3279d448 fix(mera/b2c): студия подписывалась как «0-к» на витрине сделок
Увидел на скриншоте карточки игры: «0-к, 25,9 м²». Замер на проде — 3 строки
витрины из 20 имеют rooms = 0, то есть каждая шестая карточка так и выглядит.
«0-к» читается как ошибка выгрузки, а не как тип квартиры.

Студией её называет и наш собственный бэктест (per_rooms.label в
scripts/backtest_estimator.py), и рынок.

Тест двусторонний и фальсифицирован: возврат «0-к» для rooms=0 красит его по
значению, обратная правка — снова зелено.
2026-08-29 23:25:44 +05:00
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-сервере с заглушкой бэкенда: три состояния (Кировский с
точкой, Чкаловский — кадр едет, сделка без координаты).
2026-08-29 23:25:44 +05:00
f3401badd0 feat(mera/b2c): координата сделки доезжает до витрины — с записанной границей честности
Карта лэндинга не может показать точку, пока её нет в витрине: район, комнаты,
площадь и квартал в `landing_showcase_deals` есть, координаты не было.

Что сделано: миграция 280 добавляет lat/lon (double precision, NULLABLE),
задача пересчёта переносит их из `deals`, ручка /showcase отдаёт их как
Optional[float].

ГРАНИЦА ЧЕСТНОСТИ, записанная в трёх местах (COMMENT колонок, `note` каждой
строки, докстринг задачи), а не только в голове автора: это ЦЕНТРОИД УЛИЦЫ, а
не дом. Замер на проде 2026-08-29 по той самой выборке, из которой набирается
витрина (deals, city='Екатеринбург', deal_date >= '2025-01-01'): 34 021 сделка,
34 017 с координатой, но РАЗЛИЧНЫХ точек всего 991 — ≈34 сделки в одной точке,
при 2.7% известных номеров дома. Точка верна на масштабе района и улицы и
неверна на масштабе дома; `note` едет на фронт вместе с числами, поэтому
следующий, кто возьмётся зумить карту, об этом споткнётся.

NULLABLE и без отбраковки: строка без координаты остаётся на витрине с
lat=lon=None. Выбрасывать сделку за отсутствие точки — отбор по признаку, не
связанному с качеством оценки, то есть та же порча витрины, которую здесь уже
чинили (порог по величине ошибки).

Тесты двусторонние, каждый проверен фальсификацией — краснеет по ЗНАЧЕНИЮ:
  * перестановка lat/lon в build_row  → 60.6055 == 56.8386
  * `if lat is None: return None`     → None is not None
  * перестановка lat/lon в ручке      → 60.6055 == 56.8386
  * фильтр строк без координат в ручке → len([]) == 1
Перестановку широты и долготы не ловит ни схема, ни тип (обе float), поэтому
в фикстурах намеренно непохожие величины: 56.8386 против 60.6055.

Фронтенд не тронут — его делает следующий шаг.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 23:25:44 +05:00
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 м) выбрано под масштаб карточки — она показывает
район целиком, где такая ошибка меньше толщины линии.
2026-08-29 23:25:44 +05:00
8f0c2ce1f4 Merge pull request 'feat(mera/b2c): витрина лэндинга переезжает с выдуманных чисел на замеренные' (#3243) from feat/b2c-landing-honest-copy 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 2m10s
Deploy Trade-In / deploy (push) Successful in 1m5s
Deploy Trade-In / deploy-status (push) Successful in 2s
Deploy Trade-In / perimeter-smoke (push) Successful in 14s
2026-08-29 18:25:20 +00:00
bot-backend
5f1a2ec585 perf(tradein/browser): якорная вкладка выдачи вместо переоткрытия origin на каждую карточку
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 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
CI Trade-In / browser-tests (pull_request) Successful in 1m26s
При reuse_context=true сайдкар на каждый fetch делал goto(origin), а потом
goto(url) — то есть перед каждой карточкой заново грузил страницу выдачи в той
же единственной вкладке. Смысл origin-прогрева (получить пропуск QRATOR в
контексте) при этом достигался ровно один раз, на первой карточке: дальше
контекст уже прогрет, а повторная навигация — чистая трата рукопожатия.

Ручная проверка 29.08 показала, как ходит человек: вкладка с выдачей открыта
всю сессию, объявления открываются из неё в новых вкладках. 91 карточка подряд,
0 отказов. Здесь то же самое: origin поднимается в отдельной долгоживущей
вкладке (_anchor_pages), карточки идут своими вкладками, выдача не
перезагружается.

Замер на тестовом сайдкаре (узел 11, 12 карточек): якорь поднялся ровно один
раз, 12/12 успех, медиана ~15 с против ~24 с и без роста времени к концу
прогона (раньше последние карточки уходили в 34-52 с).

Откат безопасный: не поднялась якорная вкладка — молча возвращаемся к прежнему
поведению (goto(origin) перед карточкой). Без reuse_context поведение не
меняется вовсе. Сброс контекста роняет и якорь.
2026-08-29 21:24:08 +03:00
6dd8d131ae feat(mera/estimate): характеристики дома из справочника, а не только из формы (#3242)
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 4m3s
Deploy Trade-In / build-backend (push) Successful in 1m7s
Deploy Trade-In / deploy (push) Successful in 1m16s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
2026-08-29 18:06:17 +00:00
3d51c08e44 Merge pull request 'fix(tradein/domclick): подтверждённый отказ площадки уехал в ветку «сбой транспорта» и перестал банить узел' (#3241) from fix/3239-sidecar-ban-page-reaches-ban-path into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 14s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Successful in 3m0s
Deploy Trade-In / test (push) Successful in 4m5s
Deploy Trade-In / build-backend (push) Successful in 1m36s
Deploy Trade-In / deploy (push) Successful in 8m32s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
2026-08-29 16:18:50 +00:00
8f3912cdf6 Merge pull request 'docs(tradein/domclick): два комментария описывали пул, которого нет с миграции 253' (#3240) from docs/3189-stale-pool-comments 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 Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / changes (push) Successful in 13s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / test (push) Successful in 4m6s
Deploy Trade-In / build-backend (push) Has been cancelled
2026-08-29 16:13:53 +00:00
bot-backend
c5784e85bc fix(tradein/domclick): подтверждённый отказ площадки уехал в ветку «сбой транспорта» и перестал банить узел
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (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 / browser-tests (pull_request) Successful in 1m17s
CI Trade-In / backend-tests (pull_request) Successful in 4m54s
#3237 научил сайдкар опознавать статический отказ Домклика самостоятельно —
это правильно и работает, но вместе с распознаванием ban-сигнал переехал не
туда. Отказ стал приезжать обычной 500-кой: browser_fetcher обнуляет на ней
last_response_status, и в detail.py срабатывает ветка except Exception,
которая по построению НЕ зовёт report_ban («не подтверждённый маркер-бан, а
сбой транспорта», #2600 п.4).

Итог на проде (прогон 5287): ban_kinds сменился с platform на unknown, и при
шести «статический отказ площадки» подряд в логах сайдкара в
scrape_proxy_source_bans не появилось НИ ОДНОЙ записи. Это не косметика
счётчиков — platform единственный диагноз, запускающий ротацию IP, поэтому мы
продолжали бы долбиться в отказавший узел вместо перехода на свободный.

Правка возвращает отказ на ban-путь, сохраняя разделение, ради которого
#3237 и делался:

- сайдкар кладёт в тело ошибки структурный признак ban_page и апстрим-статус.
  HTTP-код НЕ меняем: на 500 завязана classify_browser_probe;
- SidecarBanPageError — подкласс httpx.HTTPStatusError, поэтому ловля у
  прочих поставщиков и retry-политика fetch() не замечают нового типа;
- detail.py различает две ветки: подтверждённый отказ → report_ban + статус
  из исключения, транспортный сбой — как раньше.

Статус несём отдельным полем, а не через last_response_status: на error-пути
fetch() его обнуляет, а у Домклика отказ приходит с 401, без которого
классификатор ставит unknown. Подстрокой в тексте исключения признак искать
нельзя — _raise_for_sidecar_status обрезает тело до 300 символов, и
формулировка отказа менялась дважды за месяц.

Тесты держат обе ветки раздельно на всех трёх уровнях: сайдкар (признак есть
у бан-страницы, отсутствует у транспортной ошибки), фетчер (тип и
upstream_status, включая ловушку bool-как-int из #3196), detail.py
(report_ban зовётся / не зовётся, статус доезжает).

Closes #3239
2026-08-29 19:12:36 +03:00
bot-backend
3059f09e53 docs(tradein/domclick): два комментария описывали пул, которого нет с миграции 253
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
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
CI Trade-In / backend-tests (pull_request) Successful in 4m48s
Оба места утверждали, что у Домклика один выделенный резидентный прокси и
пула нет. Это перестало быть правдой ещё в #2800 (миграция 253 сняла
резервацию узла), но текст остался — и именно на него опирался тикет #3189,
поставленный под «калибровку» ограничения, которого не существует.
Свип к тому же ходит через пул давно (serp.py:359), а бэкфилл подключён к
нему в PR #3222.

Заодно докстринг называл не тот ограничитель: свип кладёт не счётчик
провалов lease, а break по первому DomClickBlockedError (#2854) — до
ротации дело не доходит ни при каком счётчике, отсюда buckets_completed=0.

Только комментарии, поведение не меняется. Миграция 175 уже применена, а
_schema_migrations трекает по имени файла без checksum — правка текста
её не перезапустит.
2026-08-29 18:58:51 +03:00
9e243fd643 Merge pull request 'fix(smoke): периметр требовал 401 от вебхука, который PR-D3 открыл намеренно' (#3238) from fix/smoke-payments-notify-after-d3 into main
All checks were successful
perimeter-smoke-mera / smoke (push) Successful in 10s
2026-08-29 15:51:07 +00:00
bot-backend
e13374e319 fix(smoke): периметр требовал 401 от вебхука, который PR-D3 открыл намеренно
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 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 / openapi-codegen-check (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
Деплой main после #3237 покраснел на perimeter-smoke:
  FAIL: trade-in payments/notify — 401 anonymous -> got '405', expected 401

Это не регрессия, а сработавшая канарейка. PR-D3 (#3231) внёс notify в
`_PUBLIC_PATHS` осознанно: это вебхук банка, он обязан быть достижим без наших
заголовков. В теле того PR прямо сказано, что 401 здесь стал бы признаком
поломки («вебхук банка получил бы отказ»). Ожидание в смоуке обновить забыли —
ровно то, о чём предупреждал комментарий над блоком в PR-D2.

Замена не ослабляет проверку, а усиливает: вместо GET→401 теперь POST→503,
что утверждает сразу два факта — маршрут существует (404 означал бы старый
образ) И приём платежей выключен (`payments_enabled=False`). 200 здесь поймает
включение флага, сделанное мимо этого смоука. Плюс GET→405 закрепляет, что путь
принимает только POST.

Проверено на живом проде: POST notify 503, GET notify 405, checkout 401,
meraocenka 404 на обоих путях; payments/payment_notifications/
payment_entitlements пусты. Полный прогон скрипта — ALL CHECKS PASSED.

Ожидания под meraocenka.ru не тронуты: PR-D4 не смержен, 404 там остаётся
канарейкой.
2026-08-29 18:49:43 +03:00
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 дн»).
2026-08-29 20:40:20 +05:00
84505b1e0b chore(site-finder): удалить мёртвый код — 14 неиспользуемых символов (#3235)
Some checks failed
Deploy / changes (push) Successful in 8s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy / build-backend (push) Successful in 2m30s
Deploy / build-worker (push) Successful in 3m39s
Deploy / deploy (push) Successful in 1m18s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Failing after 12s
2026-08-29 15:39:10 +00:00
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, откуда ту же фразу уже сняли. Правка текста —
вне участка этой задачи (файлы вёрстки правит соседняя ветка).
2026-08-29 20:39:00 +05:00
d43d8b0c60 Merge pull request 'fix(tradein/domclick): мы считали блоком собственное недоделанное рукопожатие' (#3237) from fix/domclick-challenge-not-a-block into main
Some checks failed
Deploy Trade-In / changes (push) Successful in 15s
Deploy Trade-In / build-browser (push) Successful in 2m59s
Deploy Trade-In / build-frontend (push) Successful in 3m27s
Deploy Trade-In / test (push) Successful in 4m25s
Deploy Trade-In / build-backend (push) Successful in 33s
Deploy Trade-In / deploy (push) Successful in 2m35s
Deploy Trade-In / deploy-status (push) Successful in 2s
Deploy Trade-In / perimeter-smoke (push) Failing after 10s
2026-08-29 15:38:04 +00:00
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&nbsp;₽» в FreeResultV3.

robots/noindex не тронуты.
2026-08-29 20:37:58 +05:00
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.
2026-08-29 20:35:27 +05:00
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
2026-08-29 15:32:36 +00:00
bot-backend
5391a36880 fix(tradein/domclick): недосчитанная QRATOR-страница уходила наверх как контент
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 12s
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
CI Trade-In / browser-tests (pull_request) Successful in 1m15s
Домклик отдаёт рукопожатие без единого стабильного маркера (в отличие от
Авито), поэтому _CHALLENGE_MARKERS (сняты с Авито, #3045) на нём никогда не
матчились и ветка ожидания не включалась — недосчитанная страница уезжала
наверх, парсер не находил __SSR_STATE__ и поднимал ложный блок. Это и был
двухнедельный attempted=3, blocked=3, enriched=0 у domclick_detail_backfill.

Для provider=="domclick" логика инвертирована: положительно опознаём только
два крайних состояния — успех (__SSR_STATE__) и статический отказ площадки
(«403 | Домклик» / «похоже, ваш запрос выглядит необычно»); всё остальное
(загрузчик рукопожатия, нерендеренная PoW-страница без каких-либо маркеров)
трактуется как «рукопожатие ещё идёт» и уходит в существующий
_wait_out_pow_challenge с кастомным is_pending. HTTP-статус для DomClick не
используется как сигнал (401 приходит и у отказа, и у успеха, и у здорового
рукопожатия) — решает только тело. Avito и прочие провайдеры идут по старой
elif-ветке без изменений.

_wait_out_pow_challenge получил опциональный параметр is_pending (дефолт
_is_pow_challenge) — golden-parity для всех, кроме domclick.
2026-08-29 18:31:35 +03:00
3b029cc0c7 Merge pull request 'fix(mera/b2c): выключенная ручка расчёта подтверждала своё существование и печатала схему' (#3233) from fix/disabled-endpoint-leaks-schema into main
Some checks failed
Deploy Trade-In / build-backend (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 12s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Has been cancelled
2026-08-29 15:31:26 +00:00
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
2026-08-29 15:19:24 +00:00
6ab13649f6 fix(mera/b2c): выключенная ручка расчёта подтверждала своё существование и печатала схему
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 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
CI Trade-In / backend-tests (pull_request) Successful in 5m10s
Докстринг обещал: «Отвечаем 404, а не 403: выключенная ручка не должна
подтверждать, что она существует». Замер на проде 29.08.2026 (флаг выключен):

  POST /api/public/mera/estimate  {}          → 422 + address/area_m2/rooms/consent
  POST /api/public/mera/estimate  {валидное}  → 404
  POST /api/public/mera/nosuchthing           → 401 (rbac)

422 отличается и от 404, и от 401 — то есть подтверждает, что ручка есть, и
заодно выдаёт её схему.

Причина не в логике гейта, а в его МЕСТЕ: проверка стояла первой строкой тела
хендлера, а FastAPI валидирует тело раньше, чем доходит до кода. Гейт перенесён
в dependencies=[Depends(...)] обеих ручек — зависимости решаются до разбора тела,
и выключенная ручка неотличима от отсутствующей при любом входе.

Тест двусторонний и фальсифицирован: возврат вызова в тело красит три теста
(включая уже существовавший про 404), обратная правка — снова зелено.
2026-08-29 20:19:15 +05:00
b241e0145a Merge pull request 'feat(mera/b2c): платёжный роутер, статус-машина и доставка купленного (за флагом)' (#3231) from feat/b2c-payments-router into main
Some checks failed
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m56s
Deploy Trade-In / build-backend (push) Successful in 1m3s
Deploy Trade-In / deploy (push) Successful in 2m30s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Failing after 11s
2026-08-29 15:08:33 +00:00
195f9f3697 Merge pull request 'chore(tradein): две ручки ротации без читателей и врущий комментарий над ними' (#3232) from chore/3212-dead-rotation-knobs into main
All checks were successful
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / test (push) Successful in 4m2s
Deploy Trade-In / build-backend (push) Successful in 1m40s
Deploy Trade-In / deploy (push) Successful in 1m35s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
2026-08-29 14:57:40 +00:00
7563f849db Merge pull request 'chore(tradein): четыре символа, на которые нет ни одной ссылки' (#3224) from chore/3212-dead-code-tradein into main
Some checks failed
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / test (push) Has been cancelled
Deploy Trade-In / perimeter-smoke (push) Has been cancelled
Deploy Trade-In / build-backend (push) Has been cancelled
Deploy Trade-In / build-frontend (push) Has been cancelled
Deploy Trade-In / build-browser (push) Has been cancelled
Deploy Trade-In / deploy (push) Has been cancelled
Deploy Trade-In / deploy-status (push) Has been cancelled
2026-08-29 14:57:28 +00:00
a48070dd89 fix(tradein): миграция платежей 277 → 279 (столкновение номеров) + lock_timeout
All checks were successful
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 / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m54s
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
Два параллельных агента взяли ОДИН номер: 277_landing_showcase_runs.sql в ветке
витрины и 277_payments_live_checkout_uidx.sql здесь. Обе ветки по отдельности
зелёные, но на main второй файл встал бы конфликтом — ровно ловушка из шапки
tests/test_migration_numbering.py: номер сверяется с origin/main, а не с чужими
открытыми ветками.

Заняты сейчас: 275 (метрики), 276+277 (витрина), 278 (публичный токен) → этот 279.
Ссылки на номер обновлены в payments.py и test_payments_router.py, включая путь,
по которому тест читает предикат частичного UNIQUE.

Плюс CREATE UNIQUE INDEX на существующей таблице payments обёрнут в
SET LOCAL lock_timeout = '5s' — гейт #2752.
2026-08-29 19:38:38 +05:00
c5315539fa fix(payments): один живой платёж на оценку — гарантия БД, а не порядка выполнения
Идемпотентность checkout держалась на «SELECT, потом INSERT» — ровно на том,
что шапка модуля называет дефектом. Двойной клик по кнопке оплаты давал два
параллельных запроса, два INSERT, два Init и два холда на карте покупателя.

- миграция 277: частичный UNIQUE (estimate_id, product_code) по живым статусам
  + ON CONFLICT DO NOTHING в INSERT. Проигравший гонку не идёт в банк: отдаёт
  ссылку соперника, если та уже готова, иначе 409;
- граница по времени для брошенных попыток: NEW/FORM_SHOWED старше 30 минут
  переводятся в DEADLINE_EXPIRED. Без неё зависший платёж (нотификации по нему
  может не прийти вовсе) навсегда отдавал покупателю одну и ту же протухшую
  PaymentURL. Окно НЕ распространяется на AUTHORIZED и прочие карточные
  статусы — там деньги уже в игре, разгребать их — работа реконсиляции;
- IDOR: checkout читал оценку без _assert_estimate_access. По чужому
  estimate_id возвращался order_id чужого живого платежа, а order_id — право
  доступа для /payments/status/<order_id>, отдающего capability-ссылку на
  отчёт. Проверка ставится только для оценок с владельцем: у анонимной покупки
  идентичности нет, правом там работает сам estimate_id.

Тесты двусторонние, фальсификация прогнана: снятие ON CONFLICT / границы по
времени / IDOR-гварда красит ровно один тест каждый раз, два из трёх — по
значению ответа.
2026-08-29 19:38:38 +05:00
28e13d5841 feat(payments): роутер checkout/notify, статус-машина и выдача по capability-ссылке
Не хватало ровно проводки: сервисный слой Т-Банка (PR-C) и схема (PR-B, 233)
уже были, HTTP-ручек и статус-машины — нет, как и доставки купленного.

Всё за kill-switch PAYMENTS_ENABLED (дефолт false): при выключенном контуре
каждая ручка отвечает 503 и не трогает ни банк, ни платёжные таблицы, поэтому
merge на проде не меняет поведения.

Идемпотентность целиком отдана БД (UNIQUE миграции 233 + ON CONFLICT DO
NOTHING), а не паре «проверить-потом-вставить»: между проверкой и вставкой
проходит параллельный ретрай банка, и товар выдаётся дважды. Признаком
«выдача состоялась» служит payment_notifications.processed_at, а не сам факт
строки — иначе падение процесса между записью нотификации и выдачей оставило
бы клиента без отчёта при списанных деньгах.

Доставка — capability-ссылка /api/v1/trade-in/r/<token>: токен лежит в
payment_entitlements.subject (ref_id остаётся estimate_id, на нём держится
UNIQUE «выдали один раз»), режется из GlitchTip-событий и открыт в rbac
отдельным узким префиксом. Тело GET /estimate/{id} вынесено в load_estimate,
чтобы у второго права доступа был тот же загрузчик, а не третья копия
гейта читаемости.
2026-08-29 19:38:37 +05:00
67d9efbac4 Merge pull request 'feat(mera/b2c): анонимный расчёт и повторное чтение результата по токену (за флагом)' (#3230) from feat/b2c-anon-estimate into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / deploy (push) Successful in 1m50s
Deploy Trade-In / test (push) Successful in 4m3s
Deploy Trade-In / build-backend (push) Successful in 1m5s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 12s
2026-08-29 14:37:20 +00:00
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` на каждой странице.
2026-08-29 19:35:57 +05:00
6255eccc7c fix(tradein): миграция публичного токена без lock_timeout — гейт #2752
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m53s
278 делает ALTER TABLE trade_in_estimates ADD COLUMN + CREATE UNIQUE INDEX на
ЖИВОЙ таблице (1123 строки на проде). ALTER берёт ACCESS EXCLUSIVE: без
lock_timeout он встал бы в очередь за запросами приложения и утащил их за собой.

Обёрнуто в BEGIN + SET LOCAL lock_timeout = '5s' + COMMIT по образцу
272_houses_region_code.sql.
2026-08-29 19:28:37 +05:00
dcfea2ad39 test(mera/b2c): пинит kwargs делегации анонимного расчёта, а не факт вызова
Весь анти-абузный контур публичной ручки (анонимная квота cookie+IP, семафор,
503 вместо 502, consent-гейт) держится на одном аргументе: в
app.api.v1.trade_in.estimate уходит x_authenticated_user=None. Проверял это
ноль тестов: estimate везде замокан AsyncMock, который принимает любую
сигнатуру, — подмена None на чтение заголовка запроса оставляла все 14 тестов
зелёными, а публичная форма начинала считать от чужого имени мимо квоты.

Новый тест шлёт запрос С заголовком X-Authenticated-User: admin и сверяет
фактические await_args.kwargs; заодно требует, чтобы аргументы ехали по имени
(позиционный вызов обесценивает сверку) и чтобы аргумент вообще присутствовал
(дефолт эстиматора — чужая гарантия, не наша). Проверено падением: подмена на
request.headers.get даёт «пришло: 'admin'».

Там же UPDATE токена: параметры сверялись только по хэшу, id строки — нет.
Теперь пинится result.estimate_id: токен обязан вешаться на только что
посчитанную оценку. Проверено подменой параметра — красный по значению.

test_read_filters_by_expiry_and_hash оставлен текстовым: живого Postgres с
миграцией 278 здесь нет, а поведенческий тест, ни разу не прогнанный, — это
ещё один зелёный по построению. Вместо этого в самом тесте написано, что он
проверяет (предикат есть в тексте SQL, в параметрах хэш) и чего НЕ проверяет
(сессия — MagicMock, запрос не исполняется, протухший токен не отсекается), и
чем его заменить, когда БД появится.
2026-08-29 19:28:37 +05:00
2d4daceb2f feat(mera): анонимный расчёт и капабилити-ссылка на его бесплатную часть
Публичный контур умел только подсказки и пробу покрытия: полный расчёт закрыт
RBAC, а результат анонима нельзя было прочитать повторно — _assert_estimate_access
отдаёт 404 на строку с created_by IS NULL всем, кроме админа, то есть расчёт жил
ровно в теле POST-ответа и не переживал перезагрузку страницы.

POST /api/public/mera/estimate делегирует в app.api.v1.trade_in.estimate (копии
логики нет — иначе публичная когорта разъедется с платной) и отдаёт наружу только
бесплатную часть: число аналогов и вердикт покрытия из той же coverage_probe.
Цены, прогнозы и списки аналогов остаются в БД для платного контура.

Согласие 152-ФЗ обязательно и строго True на уровне схемы, поэтому отказ
происходит до входа в хендлер — раньше, чем адрес физлица дошёл бы до БД.

POST /api/public/mera/estimate/read читает бесплатную часть по токену
(secrets.token_urlsafe(32), в БД только sha256, срок жизни 7 дней, миграция 278).
Токен едет телом: access-лог Caddy пишет URI целиком, и капабилити-ссылка в пути
легла бы в файл рядом с IP посетителя — тот же довод, по которому POST'ом сделан
/suggest. Постоянный путь заодно не требует префиксной ветки в rbac._PUBLIC_PATHS.

Всё закрыто флагом public_estimate_enabled (дефолт false → 404): включение
открывает запись ПДн и требует решения владельца вместе с правкой политики.
2026-08-29 19:28:37 +05:00
df9555b5c4 Merge pull request 'feat(mera/b2c): лента сделок и раунды игры — из реальных сделок с реальным прогнозом' (#3229) from feat/b2c-showcase-real-deals into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / deploy (push) Successful in 2m9s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
Deploy Trade-In / test (push) Successful in 4m3s
Deploy Trade-In / build-backend (push) Successful in 1m7s
2026-08-29 14:27:25 +00:00
87aa5bdf07 fix(tradein): миграции витрины без lock_timeout — CI-гейт #2752 краснел
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 13s
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 / backend-tests (pull_request) Successful in 4m54s
Обе миграции создавали индекс без транзакции и без SET LOCAL lock_timeout.
Гейт scripts/check-migration-lock-timeout.py это и поймал:

  ::error 276_landing_showcase_deals.sql:: блокирующий DDL без lock_timeout
  (CREATE INDEX IF NOT EXISTS idx_landing_showcase_deals_computed_at ...)

Обе обёрнуты в BEGIN + SET LOCAL lock_timeout = '5s' + COMMIT по образцу
272_houses_region_code.sql. Локально гейт зелёный: «проверено новых миграций: 31».
2026-08-29 19:20:04 +05:00
abe559cf8f fix(mera): витрина больше не отсеивает промахи оценщика, счётчики едут на фронт
Ревью MAJOR по честности, два пункта.

1. Убран MAX_ABS_ERR_PCT = 40 из build_row. Докстринг модуля сам запрещает
отбор по величине ошибки, но запрет был реализован только в _sort_key, а
фильтр — тот же отбор ступенькой раньше, и злее: строка не попадала даже в
кандидаты. Обоснование «отклонение >40% — почти всегда занижение ДКП ради
налога» не держится: _load_sample уже режет выборку санитарным диапазоном
₽/м² (для ЕКБ это глобальные PPM2_MIN=30k / PPM2_MAX=600k — город намеренно
не заведён в deal_city_price_bands), то есть грубые занижения вырезаны выше
по потоку и ПО СВОЙСТВУ САМОЙ СДЕЛКИ. Всё, что после этого дало большую
ошибку, — работа оценщика, и посетитель обязан её видеть. Честность про
заниженные ДКП перенесена в note каждой строки.

Заодно убраны MIN_FACT_PPM2=30k (дублировал уже применённый фильтр) и
MAX_FACT_PPM2=1.2M (недостижим при потолке выборки 600k): из трёх отбраковок
в проде срабатывала ровно одна — та, что льстила витрине, а два мёртвых
порога читались как работающие. Осталась только структурная отбраковка «нет
прогноза / квартала / площади».

2. Счётчики прогона выведены в ответ ручки. Итог пересчёта пишется в
landing_showcase_runs (миграция 277) и уезжает в ShowcaseResponse.stats
вместе с правилом отбраковки: показано 20 из N годных, рассмотрено M сделок.
Отдельная таблица, а не колонки в строках, — иначе в самом важном случае
(показывать нечего) счётчики исчезли бы вместе со строками. Ручка теперь
берёт и строки, и числа ИЗ ОДНОГО прогона: иначе пустой прогон показал бы
вчерашние строки под сегодняшними счётчиками.

Тесты двусторонние и проверены на сломанном коде: возврат любого порога по
ошибке → красный с величиной отклонения в сообщении; возврат любой границы
₽/м² → красная своя половина; stats=None при живом прогоне → красный.
2026-08-29 19:20:04 +05:00
b72dbc5da3 feat(mera): витрина лэндинга на реальных ДКП-сделках вместо выдуманных
Лента «МЕРА сказала X — продали за Y» жила на константах в marketing-v3.ts.
Здесь появляется её настоящий источник: сделки Росреестра по ЕКБ, прогнанные
через тот же спайн оценщика, что и боевой расчёт (backtest_estimator).

Отбор строк идёт по полноте данных и свежести квартала и НЕ смотрит на
величину ошибки: отбор по малой ошибке дал бы формально работающий код и
врущую витрину — показанные строки перестали бы быть выборкой из работы
оценщика. Свойство закреплено двусторонним тестом.

Витрина не показывает адреса (номер дома есть у 2.7% сделок) и не показывает
дня сделки (deal_date — первое число квартала). Каждая строка несёт note о
том, что замер не point-in-time. Заниженные ради налога ДКП отбрасываются по
|отклонению| > 40% и ₽/м² вне [30k; 1.2M], счётчик отброшенного — в лог.
2026-08-29 19:20:04 +05:00
e936ca73f8 Merge pull request 'feat(mera/b2c): метрики лэндинга считаются по проду, а не лежат литералами' (#3228) from feat/b2c-landing-stats into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 4m6s
Deploy Trade-In / build-backend (push) Successful in 1m3s
Deploy Trade-In / deploy (push) Successful in 1m19s
Deploy Trade-In / deploy-status (push) Successful in 2s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
2026-08-29 14:18:31 +00:00
2eb7814c36 Merge pull request 'fix(gendesign): FDW-роль не могла читать районы ЕКБ — витрина сделок МЕРЫ теряла район' (#3227) from fix/ekb-districts-fdw-grant into main
All checks were successful
Deploy / changes (push) Successful in 13s
Deploy / build-backend (push) Successful in 44s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 43s
Deploy / deploy-caddy (push) Has been skipped
Deploy / deploy (push) Successful in 1m8s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 13s
2026-08-29 14:15:45 +00:00
e9a2fff0b3 test(mera/b2c): гейт на сам SQL доли снижений + чистка протухших метрик
All checks were successful
CI Trade-In / changes (pull_request) Successful in 13s
CI / changes (pull_request) Successful in 16s
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
CI Trade-In / backend-tests (pull_request) Successful in 5m32s
Ревью: гейт охранял не то место. Подмена знаменателя красила три теста, но
дефект «84.8% вместо 48.1%» живёт в SQL — во включении однострочных записей
истории (у domklik одна запись = «цену не менял») в знаменатель. Ревьюер
вернул дефект условием n_rows >= 2 в CTE moved, и все 25 тестов остались
зелёными: текстовые пины держали только span_days и max_abs_pct.

Новый пин держит обе половины: однострочные попадают в moved веткой CASE со
значением 0, и нигде в запросе нет фильтра по числу записей истории (ни в
WHERE, ни HAVING). Живой прогон на подготовленных строках не заведён
намеренно: DATABASE_URL в тестовой джобе — заглушка, Postgres там нет, и тест
по образцу test_purge_expired_trade_in_data.py молча скипался бы, то есть не
гейтил бы ничего. Фальсифицировано руками — с n_rows >= 2 тест красный и
называет причину.

Второе: метрика, у которой пропал вход, больше не доживает в таблице со
старым computed_at (ручка отдавала её неотличимо от свежей). Строки вне
сегодняшнего набора удаляются в той же транзакции. На ПУСТОМ наборе чистка
не ходит: разом отвалившиеся все входы — признак поломки прогона, а не пяти
одновременных «данных больше нет». Оба поведения покрыты тестами, оба
проверены на сломанном коде.
2026-08-29 19:10:04 +05:00