Commit graph

1365 commits

Author SHA1 Message Date
bot-backend
c1918b404d test(tradein/avito): тест закреплял отсутствие проводки пула
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 9s
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 4m46s
`assert_called_once_with(source=..., endpoint=...)` требует, чтобы других
аргументов у вызова НЕ БЫЛО. Тем самым тест закреплял ровно тот дефект, который
чинит этот PR: браузерный фетчер обязан был строиться без проводки пула, иначе
CI краснел.

Заменено на проверку вхождения: source и endpoint по-прежнему сверяются, плюс
явно требуется наличие proxy_provider/use_pool/environment — то, без чего
прогон уходит мимо пула (proxy_lease_id=None, 5 блоков из 5 при здоровом пуле).

Прогон: 277 тестов зелёные, ruff чист.
2026-08-27 16:16:26 +03:00
bot-backend
d66a52d492 fix(tradein/avito): браузерный бэкфилл ходил мимо пула прокси
Some checks failed
CI Trade-In / changes (pull_request) Successful in 8s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
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 / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Failing after 4m53s
Ветка browser_mode в avito_detail_backfill конструировала BrowserFetcher без
proxy_provider/use_pool/environment. Без них фетчер не кладёт "proxy" в тело
POST /fetch, сайдкар берёт свой env-прокси, и прогон уходит мимо пула целиком:
ни выбора узла по affinity, ни учёта scrape_proxy_source_bans, ни ротации при
блоке. В логе это ровно `proxy_lease_id=None`.

Ровно этот дефект чинили рядом — #2698 в house_imv_backfill, где он держал
35 отказов из 35 попыток в каждом прогоне полтора месяца, пока соседние свипы
через ТОТ ЖЕ сайдкар тянули сотни объявлений. Здесь он остался.

Замер 27.08, прогон 5098:

    mode=browser, proxy_lease_id=None
    BLOCKED #1..#5 подряд — firewall/soft-block (browser-mode)
    ABORT — 5 consecutive blocks, enriched=0 attempted=5

При этом пул здоров — 4 узла, все ok, ни один не занят, браузерная проверка
пройдена в то же утро. А тот же URL Авито через прокси отдаёт 200 и 3.3 МБ
страницы. То есть площадка нас пускала, запрос шёл не оттуда.

Это объясняет, почему предыдущая правка (#3143, прокси в повторах curl-пути)
не восстановила сбор: боевой режим бэкфилла — browser, и он до curl-веток
вообще не доходит.

Три теста: конструктор на месте (страховка от проверки пустоты), все три
аргумента проводки передаются, use_pool читается из конфига а не зашит
константой (зашитый True отнял бы у владельца выключатель, зашитый False вернул
бы дефект незаметно). Проверил красноту на коде без проводки.
Прогон: 113 тестов зелёные, ruff чист.

Refs #3045, #3034, #2698
2026-08-27 15:58:26 +03:00
bot-backend
26cd8dbe34 fix(tradein/avito): повтор после блока терял прокси и уходил с адреса машины
All checks were successful
CI / changes (pull_request) Successful in 10s
CI Trade-In / changes (pull_request) Successful in 8s
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 Trade-In / backend-tests (pull_request) Successful in 4m52s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Обе ветки повтора в `fetch_detail` — 403/firewall и 429 — пересоздавали
эфемерную сессию вызовом `_build_detail_session()` БЕЗ `config`. Прокси
kit-версия читает только из `config.scraper_proxy_url`, поэтому такая сессия
уходила напрямую.

Замысел ветки прямо обратный, он записан в её же комментарии: «эфемерная
свежая сессия (новый CONNECT-туннель = свежий exit-IP)». Ветка вообще
исполняется только при backconnect=True, а он вычисляется как «задан
config.scraper_proxy_url» — то есть в момент вызова достоверно известно, что
прокси есть, и он терялся.

ПОЧЕМУ НЕ БЫЛО ВИДНО. Пока адрес самой машины не был заблокирован, прямой
повтор часто срабатывал, и подмена канала выглядела как успех. Замер 27.08 из
прод-контейнера, один и тот же URL Авито:

    через прокси — 200, 3.3 МБ страницы
    напрямую     — 429, «доступ ограничен», firewall

С этого момента каждый повтор после блока обречён. Обогащение
avito_detail_backfill по суткам: 21-25.08 — 178/129/147/138/111, 26.08 — 23,
27.08 — 0 при 25 блоках. Обвал начинается ровно с окна, в котором сменился
адрес машины.

Тот же класс ошибки чинили в #2330 для build_warmed_session; в пути повтора он
оставался.

Три теста: обе ветки на месте (страховка от проверки пустоты), ни одна не
строит сессию без config, и отдельно доказано, что без config прокси в сессии
действительно нет. Проверил красноту на старом коде — падает с точным текстом.
Прогон: 107 тестов scrapers зелёные, ruff чист.

Refs #3034, #3045
2026-08-27 15:33:29 +03:00
ede5653aba feat(tradein/domclick): чекпоинты для city_sweep — корзина как единица возобновления (#3118, #3074)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / 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 / 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 4m43s
Прод-факты #3118: QRATOR рубит свип внутри 1-2-й корзины при любом старте
(buckets_completed ≤ 1 из 6), banned-прогоны собирают 59–1815 лотов. Сдвиг
#2854 лишь распределяет потери; чекпоинт превращает случайную ротацию в
систематический обход — шесть прогонов закрывают шесть корзин.

Моё раннее «домклику чекпоинтить нечего» (замер в #3074 по средним 3 мин)
опровергнуто данными #3118 — этот коммит исправляет тот вывод кодом.

Зеркально yandex-чекпоинту (#3074): провайдер — skip_buckets + имена
завершённых корзин + гард «цепочка накопила все 6» (честный no-op);
buckets_total сжимается до объёма ЭТОГО прогона; пайплайн — done_buckets =
унаследованное ∪ завершённое, heartbeat с мержем jsonb до финализации;
планировщик — generic _pick_resume (#2845).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 16:52:44 +05:00
bot-backend
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 чист.
2026-08-27 14:37:05 +03:00
bot-backend
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 чист.
2026-08-27 14:29:19 +03:00
01b36f623e Merge pull request 'fix(tradein): срок жизни оценки 24ч → 720ч — отчёты исчезали раньше, чем ими успевали воспользоваться' (#3133) from fix/tradein-retention-720h into main
All checks were successful
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 2m29s
Deploy Trade-In / test (push) Successful in 4m3s
Deploy Trade-In / build-backend (push) Successful in 1m5s
Deploy Trade-In / deploy (push) Successful in 1m29s
Deploy Trade-In / deploy-status (push) Successful in 5s
Deploy Trade-In / perimeter-smoke (push) Successful in 13s
2026-08-27 10:51:15 +00:00
88e23c12a5 Merge pull request 'fix(tradein/auth): лимит попыток входа 5 → 20 на адрес — офис за одним NAT запирал сам себя' (#3132) from fix/login-rate-limit-office-nat into main
Some checks failed
Deploy Trade-In / test (push) Blocked by required conditions
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) Has been cancelled
2026-08-27 10:51:09 +00:00
f9477b1c84 Merge pull request 'fix(mera/ui): карта перестала перекрывать интерфейс, страница помещается в окно' (#3131) from fix/mera-ui-stacking-and-fit into main
Some checks failed
Deploy Trade-In / test (push) Blocked by required conditions
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) Has been cancelled
2026-08-27 10:51:01 +00:00
bot-backend
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: это классификация
устройства, а не расчёт геометрии.
2026-08-27 13:42:56 +03:00
49683b7800 feat(tradein/db): дроп дублирующего listings_geom_idx — geography-двойник несёт всю работу (#2997)
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 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 4m45s
Критерий (размечен 21.08 ДО окна) выполнен, окно склеено через переезд:
замороженный Beget — 4 116 сканов против 217 712 у geog за всю историю
(прирост +164/4д — вырожденные IS NOT NULL-планы, паттерн #3020); живой
Poincare — 0 сканов за двое суток; EXPLAIN горячего пути — geog; свип кода —
ни одного пространственного оператора по голому listings.geom.

Экономия 115 МБ + минус одна индексная запись на каждый не-HOT апдейт
(их 20.86 млн). CONCURRENTLY по образцу 270, идемпотентно, откат — CREATE
INDEX CONCURRENTLY (в шапке миграции).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 15:41:27 +05:00
bot-backend
34f8f506c0 fix(tradein): срок жизни оценки 24ч → 720ч (30 суток) — отчёты исчезали раньше, чем ими успевали воспользоваться
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 13s
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 4m50s
27.08, пилот «Практика». У пользователя ВСЕ 75 оценок оказались
недоступны, включая позавчерашние: список показывал их со статусом
«устарел», а открытие отдавало «Ссылка устарела, отчёт удалён или у вас нет
к нему доступа». Выглядело как пропажа данных — на деле строки целы, истёк
expires_at = created_at + 24ч.

Совпадение двух вещей и создало впечатление аварии: сутки не работал вход
(упало право CONNECT на базу auth), и ровно за это время истекли последние
живые отчёты. Люди зашли и увидели, что не открывается ничего.

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

Настройка одна на оба контура, и это осознанно НЕ маскируется: юридический
мотив 152-ФЗ относится к анонимному B2C, разделение сроков по контурам —
отдельная задача. Здесь поднят общий срок, а не сделан вид, что контуры уже
разведены.
2026-08-27 13:31:40 +03:00
bot-backend
b9654d01c0 fix(tradein/auth): лимит попыток входа 5 → 20 на адрес — офис за одним NAT запирал сам себя
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI / changes (pull_request) Successful in 14s
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 4m55s
27.08, инцидент с пилотом «Практика». После починки доступа к базе auth
сотрудники начали получать «Слишком много попыток» — при том, что многие
ещё вообще не пробовали войти.

Причина в форме лимита, а не в его величине. Пилот — ОФИС: все выходят
из-под одного NAT, и пять попыток за пять минут делились на всю компанию
сразу. Одного человека, перепутавшего пароль, хватало, чтобы запереть
остальных.

Защита от перебора не ослабевает. Настоящий предохранитель — счёт по
ЛОГИНУ (login_username_fail_threshold, 20 за час), он не тронут. Лимит по
адресу существует против всплеска с одной машины, а не против офиса, и
двадцать попыток за пять минут эту роль выполняют.
2026-08-27 13:26:44 +03:00
3c315b5105 fix(tradein/scheduler): boot-reap виден и при нуле — молчащий механизм неотличим от неподключённого (#3122)
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 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 4m45s
Первая живая приёмка #3122: деплоевский startup-reap успел снять зомби
раньше, boot-reap отработал с нулём — и не оставил НИКАКОГО следа
исполнения. Ноль — штатный исход, но «сторож, молчащий при нуле» — это
слепая зона по построению: оборванную проводку не отличить от чистого
прода. INFO-строка при нуле, warning при снятых — как было.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 14:33:06 +05:00
63956f0891 feat(tradein/scheduler): boot-reap — прогоны предыдущего контейнера снимаются на старте (#3122)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 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 4m40s
Прод-факт 27.08 (после ночного офлайна #3119): 4 прогона 'running' со
стартами до старта контейнера блокировали свои источники через
has_running_run до 6-часового порогового reap'а — до пяти часов слепоты
на источник ровно после простоя, когда догон нужнее всего.

Критерий — started_at < старт процесса планировщика (минус минута на
дрейф), пульс не участвует: ложные срабатывания класса #2702 (редкий
пульс длинных прогонов) невозможны по построению — живой прогон этого
процесса не может быть старше самого процесса.

Маркер counters.boot_reaped=true открывает boot-зомби подхват чекпоинта
(_resume_decision): у порогового zombie процесс может быть жив
(движущаяся точка — причина исключения 'zombie' из _RESUME_STATUSES),
у boot-зомби — гарантированно мёртв. Пороговый zombie без маркера
по-прежнему отвергается (закреплено тестом-инвариантом).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 14:17:42 +05:00
27fdd55aae feat(tradein/matching): region_code у houses — вывод, а не выдумка (#3051 п.4)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
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 4m32s
Миграция 272: колонка + бэкфилл ТОЛЬКО по bbox региона 66 (значения
байт-в-байт из реестра, синхронизацию держит тест). Карантин NULL:
620 домов без geom, 23 порченых (ЕКБ-адреса с чужими координатами —
«Вильгельма де Геннина» на Байкале, «Крауля» под Москвой, «Учителей» в
Таллине, с живыми ссылками листингов) — им регион не присваивается,
включая 3 дома с координатами в bbox Москвы (порча, не переезд). NOT NULL
из постановки — отдельной миграцией, когда карантин опустеет.

Запись: новый дом наследует регион РАЗВЁРТКИ (base.py → контракт →
адаптер → matching), только если координаты не противоречат; координаты
другого региона → NULL-карантин + warning. Вне-bbox координаты при живой
развёртке наследуют её регион (адрес и развёртка согласны, координатам
веры нет) — семантика закреплена тестом, чтобы смена была осознанной.

Матчинг-запросы НЕ тронуты — гард по региону это #3052.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 13:51:48 +05:00
8a4215fe20 Merge pull request 'feat(tradein/geo): реестр регионов — единственный источник гео-границ (#3051, шард 1a)' (#3116) from feat/3051-region-registry into main
All checks were successful
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m51s
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / deploy (push) Successful in 7m42s
Deploy Trade-In / perimeter-smoke (push) Successful in 9s
Deploy Trade-In / build-backend (push) Successful in 57s
Deploy Trade-In / deploy-status (push) Successful in 1s
2026-08-26 15:23:59 +00:00
bot-backend
68daac2a30 feat(tradein/scraper): чекпоинт по якорям для cian_city_sweep (#3074)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
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 / changes (pull_request) Successful in 9s
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m43s
Третий шард после yandex (#3098) и avito (#3112). Выбран по замеру за 60 дней:
65 прогонов, среднее 35 минут, максимум 72, две отмены деплоем. Пятиминутного
дренажа (#3029) на такие прогоны не хватает - убитый на 35-й минуте сбор
начинался заново с первого якоря.

Ключ чекпоинта - ИМЯ якоря, а не индекс: состав списка зависит от city_slug
(областные свипы идут по своим наборам), позиция между городами не устойчива.
По той же причине гарда по числу якорей не нужна - в отличие от combo-чекпоинта
яндекса, где ключ якоря не содержал.

Отличие от avito-шарда: там успех и неудача якоря сходились в одной строке и
потребовался отдельный флаг _anchor_ok. У циана граница уже проведена самим
потоком управления - все ветки отказа делают return или continue и до записи
чекпоинта не доходят. Добавлять флаг значило бы дублировать то, что уже
выражено структурой; достаточно писать чекпоинт в единственной точке успеха.
Тест сторожит эту границу отдельно, потому что рефакторинг, сливающий ветки,
сломал бы её незаметно.

Пропущенный якорь двигает anchors_done - чтобы счётчик продолжал означать
"докуда дошли по списку", а не "сколько собрал именно этот прогон".

Тесты (4) поведенческие, с подменой CianScraper и save_listings: якорь из
чекпоинта не опрашивается вовсе; пройденный дописывается поверх унаследованных;
без чекпоинта обходятся все; упавший в чекпоинт не попадает. Двойнику пришлось
добавить счётчики state_extraction_* - конвейер читает их после каждого якоря
(#2625), и без них падал бы сам двойник, а не проверяемая логика.

Фальсификация: на исходном коде краснеют все 4. Весь набор #3074 (yandex, avito,
cian, claim) - 14 passed.
2026-08-26 17:21:58 +03:00
24369a6bf2 test(3051): tests/services/test_location_index.py переведён на _covered_region
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-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) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m38s
CI поймал то, что мой локальный прогон пропустил: сьют в ПОДДИРЕКТОРИИ
tests/services/ звал удалённый _in_ekb_bbox. Граничные точки сохранены те же
(продукт-ядро 66 байт-в-байт), проверка дополнена кодом региона.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 18:47:29 +05:00
0529bb249c feat(tradein/geo): реестр регионов — единственный источник гео-границ (#3051, шард 1a)
Some checks failed
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
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 Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Failing after 4m39s
Границы покрытия лежали литералами в трёх файлах (location_index / geocoder /
matching.normalize), и каждая молча отвергла бы Москву. Новый модуль
app.services.regions — лист дерева импортов — держит per-регион bbox'ы
(tight/wide/region/product_core), города, city_token и набор доступных тиров
обогащения; потребители держат прежние имена как алиасы на объекты реестра
(identity закреплена тестом — копии, разъезжающиеся при правке, невозможны).

Регион 66 — байт-в-байт прежние литералы (закреплено тестом: этот PR только
переносит границы, менять их = отдельное решение). Регион 77 (Москва): МКАД-
ядро + генеральный bbox с Новой Москвой и Зеленоградом; тиров обогащения НЕТ
ни одного — и это явный факт реестра с готовой формулировкой
(unsupported_tier_reason), а не молчаливое «посчитаем без источника».

Приёмка #3051: точка 55.75/37.62 больше не out_of_coverage — location_index
узнаёт регион 77 и считает в его ядре (сегодня листингов Москвы нет → честный
insufficient_data). Область 50 отложена по решению в #2996.

Не здесь (следующие шарды): city_fias_id сквозняком (п.2), doc_type в deals
(п.3), region_code у houses (п.4), депромоут описаний (п.5), параметры
загрузчиков (п.6). Гейт ЕКБ-тиров геокодера (#2582) уже деградирует правильно
для Москвы — fail-closed открывает их только при подтверждённом ЕКБ.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 18:39:28 +05:00
7a5997b059 test(2656): краснота на main — по значению, не ImportError
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 / backend-tests (pull_request) Successful in 4m39s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
getattr вместо прямого импорта LISTINGS_FRESH_DAYS из config: на origin/main
константы там ещё нет, и тест умирал ImportError'ом на сборке модуля —
«возможности нет» вместо «значение неверно». Теперь на main: 5 красных
ассертами (предикат отсутствует/окно None/протухшие комплы в пуле), 3 зелёных
(сброс anchor_tier уже влит отдельно).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 18:25:54 +05:00
3d0541b2e2 Merge origin/main into fix/2656-anchor-ratio-freshness — конфликты: метки #2656→#2661 (main), импорт-union, baseline unrecorded=0 (перезахваченная фикстура) 2026-08-26 18:22:54 +05:00
bot-backend
0586951772 feat(tradein/scraper): чекпоинт по якорям для avito_city_sweep (#3074)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m47s
Продолжение после yandex-свипа. Выбор источника — по замеру, а не «для
полноты»: за 60 дней avito_city_sweep дал 68 прогонов, 25 банов и 3 отмены
деплоем при среднем времени 15 мин и максимуме 97.

Прод-факт, который решает дело. Типичный итог свипа:

    {"anchors_done": 1, "anchors_total": 5, ...,
     "enrichment_abort_note": "detail enrichment aborted (Avito detail
      firewall/soft-block ...)"}

Прогон срывается блокировкой на ПЕРВОМ из пяти якорей. Без чекпоинта
следующий прогон снова идёт в первый якорь, упирается в ту же стену, и якоря
2-5 не собираются никогда.

Ключ чекпоинта — ИМЯ якоря, а не индекс: состав списка зависит от city_slug,
позиция в нём между городами не устойчива. По той же причине здесь не нужна
гарда по числу якорей, которая есть у combo-чекпоинта яндекса: там ключ
якоря не содержал, здесь якорь и есть ключ.

Инвариант, ради которого отдельный флаг _anchor_ok: в чекпоинт попадает
только якорь, пройденный до конца. Ветка блокировки делает return и до записи
не доходит, а generic-except доходит — якорь упал, но цикл продолжается.
Записать такой якорь пройденным значило бы, что следующий прогон пропустит
его навсегда, причём молча: прогон завершится штатно, просто часть города не
соберётся. Флаг сбрасывается на каждой итерации, иначе один упавший якорь
заразил бы все последующие.

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

domclick_city_sweep намеренно НЕ трогаю: 54 прогона, ноль отмен деплоем,
среднее время 3 минуты — чекпоинт там не окупается. cian_city_sweep (среднее
35 мин, 2 отмены) — следующий шард.

Тесты (4) поведенческие: якорь из чекпоинта не опрашивается вовсе; пройденный
дописывается поверх унаследованных; без чекпоинта обходятся все; упавший в
чекпоинт НЕ попадает. Оговорка: на исходном коде они падают по сигнатуре
(unexpected keyword argument), то есть доказывают отсутствие параметра, а не
поведение — поведенческую часть держат сами проверки. Весь набор #3074 —
10 passed.
2026-08-26 14:52:46 +03:00
bot-backend
124cfb3d5d feat(observability): /metrics в обоих бэкендах — счётчики, задержка, дашборд
All checks were successful
CI / backend-tests (pull_request) Successful in 17m30s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m22s
CI Trade-In / backend-tests (pull_request) Successful in 4m51s
Третья часть #3078 и единственная, трогающая прод-код.

До неё числовых рядов у приложений не было вовсе: только логи и исключения в
GlitchTip. Класс отказов «отвечает, но медленно» и «отдаёт 401 потоком» в такой
картине невидим — исключения нет, строка в логе выглядит обычной, а продукт
при этом не работает.

Метка route — ШАБЛОН маршрута, а не путь запроса. Это несущее решение, а не
деталь: кадастровый номер или идентификатор заявки в метке даёт новый временной
ряд на каждую сущность, а ряд у Prometheus стоит памяти постоянно, а не в момент
запроса. Самый известный способ уронить мониторинг тем самым мониторингом.
Незаматченные пути (404, сканеры) сведены в одну метку, иначе тот же взрыв
устроит любой бот, перебирающий адреса. Оба свойства сторожатся тестами, а не
комментарием: тест бьёт тремя разными идентификаторами и требует ОДИН ряд.

Слой регистрируется последним и потому оказывается самым внешним. Изнутри
RBAC-гварда не видно ни отказов авторизации, ни времени, которое он тратит на
резолв сессии в БД auth, — а именно этот путь уже давал инцидент с блокирующим
I/O в middleware (#1202). Упавший исключением запрос считается как 500 в
finally: без этого он просто отсутствовал бы в счётчике, то есть ровно тогда,
когда метрики нужнее всего.

Путь публичен ВНУТРИ и закрыт СНАРУЖИ — это два разных периметра. Скрейп идёт
из docker-сети, где заголовка X-Authenticated-User нет ни у кого, поэтому
/metrics внесён в _PUBLIC_PATHS обоих бэкендов; иначе агент получал бы 401 и
метрик не было бы вовсе. Наружу путь не открывается ни через gendsgn.ru, ни
через meraocenka.ru, и вдобавок закрыт явным respond 404 в обоих site-блоках —
чтобы закрытость осталась решением, а не следствием текущего порядка директив.

Ограничитель частоты и аудит «Меры» не трогались: оба смотрят только на пути
под /api/, скрейп под них не попадает. Проверено тестом, а не чтением.

Прод-поведение не меняется ничем, кроме нового публичного пути: ни один
существующий обработчик, гвард или маршрут не тронут.

Refs #3078
2026-08-26 11:30:18 +03:00
2116c3774e Merge pull request 'fix(mera/ui): карточка прогресса перестала выдумывать стадии (#3081, вариант A)' (#3101) from fix/3081-honest-progress into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / deploy (push) Successful in 1m18s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / build-frontend (push) Successful in 2m29s
Deploy Trade-In / test (push) Successful in 3m51s
Deploy Trade-In / build-backend (push) Successful in 35s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
2026-08-26 08:01:56 +00:00
01ccf4f133 Merge pull request 'feat(tradein/yandex): is_pro_seller из агентского блока + бэкфилл (#3063 п.2)' (#3100) from fix/3063-pro-seller-derive into main
Some checks failed
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) Successful in 3m48s
Deploy Trade-In / deploy (push) Has been cancelled
Deploy Trade-In / build-backend (push) Successful in 1m48s
2026-08-26 07:55:37 +00:00
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>
2026-08-26 12:54:32 +05:00
993b44c7e0 Merge pull request 'feat(tradein): чекпоинты #3074 — наследование при claim + yandex_city_sweep' (#3098) from fix/3074-checkpoint-survives-claim into main
Some checks failed
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-backend (push) Successful in 1m33s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m46s
Deploy Trade-In / deploy (push) Has been cancelled
2026-08-26 07:49:28 +00:00
1aaa8b88a1 feat(tradein/yandex): is_pro_seller выводится из агентского блока + бэкфилл 4469 строк (#3063 п.2)
All checks were successful
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m29s
Detail-обогащение извлекало agency_name, но признак «продаёт профи» не
выводило — 4 469 активных листингов с известным агентством стояли с
is_pro_seller=NULL (признак эрозирован SERP-затиранием до PR #3067, а
заново не появлялся). Признак идёт в оценщик trade-in.

- save_detail_enrichment: is_pro_seller=TRUE при известном agency_name,
  fill-only COALESCE; отсутствие блока НЕ доказывает «частник» (п.3 задачи —
  отдельное решение), в ту сторону ничего не пишем;
- миграция 271: одноразовый бэкфилл уже существующих строк всех источников
  (правило источник-независимо), идемпотентна.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 12:48:18 +05:00
307055232e feat(tradein/yandex): чекпоинты для yandex_city_sweep — combo как единица возобновления (#3074)
All checks were successful
CI / changes (pull_request) Successful in 9s
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 Trade-In / changes (pull_request) Successful in 7s
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 4m35s
Из таблицы убитых деплоем: yandex_city_sweep 15.08 прожил 65 мин, 12.08 —
2ч33м; оба потеряны целиком — у свипа не было чекпоинтов вовсе.

Единица чекпоинта — combo (сегмент × комнатность × ценовой диапазон), ровно
то, чем цикл обхода уже итерируется. Три слоя:
- провайдер: skip_combos (ни одного HTTP по собранным) + on_combo для каждого
  ПРОЙДЕННОГО combo, включая пустые — иначе пустой combo не попадал бы в
  чекпоинт и перечитывался бы вечно; оборванный отказом combo (gate failure)
  on_combo по-прежнему не вызывает;
- пайплайн: done_combos → heartbeat с done_buckets (мерж jsonb, финализаторы
  не затирают); подхват гейтится единственным якорем — combo_label не содержит
  якоря, multi-anchor подхват пропускал бы чужие якоря;
- планировщик: resume_run_id=_pick_resume(...) в диспатче (generic-механизм
  #2845 — params-идентичность, свежесть точки, потолок цепочки — бесплатно).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 12:43:58 +05:00
929eff11d7 Merge pull request 'feat(tradein/estimate): потолок одновременных оценок — 4 слота, быстрый 429 (#3082)' (#3097) from fix/3082-estimate-concurrency 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 / test (push) Successful in 3m42s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-backend (push) Successful in 1m16s
Deploy Trade-In / deploy (push) Successful in 1m25s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
2026-08-26 07:36:37 +00:00
e4b3c6cc2b fix(tradein/scheduler): чекпоинт предшественника переживает обрыв до первой новой корзины (#3074)
All checks were successful
CI / changes (pull_request) Successful in 10s
CI Trade-In / changes (pull_request) Successful in 9s
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 4m35s
Прод-факт: прогон 4707 (23.08) подхватил 42 корзины у 4117, был убит деплоем
на 26-й минуте до завершения первой НОВОЙ корзины — и не успел ни разу
написать heartbeat с done_buckets. Его собственный чекпоинт пуст: следующий
кандидат увидел бы no_checkpoint, цепочка оборвалась бы с потерей 42 корзин.

_resume_decision при вердикте 'ok' теперь кладёт done_buckets предшественника
в counters-заготовку нового прогона — она персистится при claim, до старта
пайплайна. Heartbeat мержит jsonb: первый настоящий bucket-heartbeat
перезапишет ключ надмножеством, двойной записи нет. Отказные вердикты
чекпоинт не наследуют (прогон с нуля не должен врать о собранном).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 12:35:44 +05:00
4b245b6605 feat(tradein/estimate): потолок одновременных оценок — 4 слота, быстрый 429 (#3082)
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 / 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 4m41s
Рейт-лимит меряет частоту (300/60с вправе стартовать в одну секунду), квота —
счётная и помесячная: параллелизм /estimate не ограничивал никто. Оценка
0.8–2.4с держит соединение общего пула SQLAlchemy (5+10) и внешние тиры —
пила одновременных оценок выедала пул и тормозила весь /api/v1/*.

Семафор по образцу public/mera.py::_suggest_slots: acquire после дешёвых
отказов (рейт-лимит, квота) с ожиданием 5с ≈ две длительности оценки,
timeout → 429 с Retry-After; release в finally сразу после дорогой части.
4+4 слота (estimate+suggest) = 8 удерживаемых соединений из 15 пула.

Семафор в памяти процесса — при переходе на несколько воркеров (#3083)
лимит умножится на их число; задачи согласовывать (о чём комментарий на месте).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 12:29:31 +05:00
0a85fa7202 fix(tradein/observability): результат detail-бэкфиллов доезжает до колонки total_seen (#3044)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 9s
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 4m37s
Прод-факт: замер по avito_detail_backfill прочитал «обогащено 0 за 7 дней»
при реальных 801 — джобы пишут результат только ключом 'enriched', которого
_column_counts не знал, и колонка total_seen оставалась 0 у всех прогонов.

'enriched'-фолбэк добавлен именно в _column_counts (витринная колонка), а НЕ в
_RESULT_COUNTER_KEYS: тот список кормит zero-result-стрик, где догнавший
очередь бэкфилл стал бы непрерываемым измеренным нулём — ловушка, за которую
ревью уже выкинуло из списка 'rows_inserted' (#2703). Контракт стрик-сторожа
закреплён инвариант-тестом.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 12:23:19 +05:00
729e9acc52 fix(tradein/deploy): подключить selectel-оверрайд — иначе после переезда бот умрёт молча (#3093)
All checks were successful
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / build-frontend (push) Successful in 2m42s
Deploy Trade-In / build-browser (push) Successful in 3m16s
Deploy Trade-In / deploy (push) Successful in 3m15s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 9s
Deploy Trade-In / test (push) Successful in 3m52s
Deploy Trade-In / build-backend (push) Successful in 34s
2026-08-25 16:39:40 +00:00
d409e31f00 feat(mera/b2c): анти-абуз для анонимного трафика — этап 2 из 8 (#2546)
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 3m50s
Deploy Trade-In / build-backend (push) Successful in 4m23s
Deploy Trade-In / deploy (push) Successful in 2m32s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
2026-08-25 16:27:02 +00:00
2feb0de446 fix(ops): бэкап без выгрузки в S3 падает, а не рапортует успех (#3086)
All checks were successful
Deploy Infra Host / sync-infra-host (push) Successful in 2s
Deploy / changes (push) Successful in 7s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy Trade-In / changes (push) Successful in 12s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy Trade-In / build-browser (push) Successful in 45s
Deploy / deploy (push) Successful in 1m45s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Successful in 2m35s
Deploy Trade-In / test (push) Successful in 3m53s
Deploy Trade-In / build-backend (push) Successful in 34s
Deploy Trade-In / deploy (push) Successful in 2m21s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
2026-08-24 17:50:18 +00:00
7c916082e8 feat(tradein/domklik): ЖК в свою колонку вместо только raw_payload (#3076)
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 / build-backend (push) Successful in 1m47s
Deploy Trade-In / deploy (push) Successful in 1m55s
Deploy Trade-In / perimeter-smoke (push) Successful in 9s
Deploy Trade-In / test (push) Successful in 3m46s
Deploy Trade-In / deploy-status (push) Successful in 1s
2026-08-24 16:04:24 +00:00
9810ae350f feat(tradein/deactivate-stale): страховочные рельсы объёма снятия (#3066)
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / test (push) Successful in 3m47s
Deploy Trade-In / build-backend (push) Successful in 1m43s
Deploy Trade-In / deploy (push) Successful in 2m1s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
2026-08-24 08:44:32 +00:00
91731ddb5f fix(tests): объявить два живых теста #3063 в skip_allowlist (#3069)
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 2m24s
Deploy Trade-In / test (push) Successful in 3m42s
Deploy Trade-In / build-backend (push) Successful in 1m33s
Deploy Trade-In / deploy (push) Successful in 1m58s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 9s
2026-08-23 22:14:49 +00:00
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
2026-08-23 21:52:48 +00:00
e73cde7ad3 fix(scraper-kit): бедный re-scrape больше не стирает признаки продавца (#3067)
Some checks failed
Deploy Trade-In / test (push) Blocked by required conditions
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) Has been cancelled
2026-08-23 21:52:38 +00:00
4aaa021b62 feat(tradein/domklik): пробросить isRosreestrApproved в колонку (#3068)
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-backend (push) Blocked by required conditions
Deploy Trade-In / changes (push) Successful in 13s
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-23 21:49:43 +00:00
3b6bca80c0 fix(avito): считать HTTP 439 блокировкой, а не концом выдачи (#3065)
Some checks failed
Deploy Trade-In / build-backend (push) Blocked by required conditions
Deploy Trade-In / build-frontend (push) Blocked by required conditions
Deploy Trade-In / deploy-status (push) Blocked by required conditions
Deploy Trade-In / test (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 / changes (push) Has been cancelled
2026-08-23 21:49:35 +00:00
bot-backend
05e82989a8 fix(ops): закрепить рабочий адрес api.telegram.org на новом хосте
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 / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Замер 2026-08-23 с Poincare: у api.telegram.org семь публикуемых адресов,
отвечает РОВНО ОДИН — 149.154.167.220 (302 за 0.11 с). Резолвер при этом
отдаёт 149.154.166.110, который мёртв. Скан всего 149.154.167.0/24
подтвердил: живой он один во всём блоке.

Это не блокировка Selectel и не наш файрвол: с Beget отвечают три адреса
из тех же семи, включая тот, что отдаёт DNS там. Telegram частично
фильтруется у ОБОИХ провайдеров, просто Beget попадает на живой адрес.
Через прокси ASocks Telegram не проходит ни с одного из четырёх узлов
(все 000) — узлы российские, путь закрыт.

Без закрепления tradein-tgbot на long-polling резолвит мёртвый адрес,
виснет и умирает МОЛЧА: restart поднимает его заново, он снова виснет,
ни краша, ни строки в логе.

Закреплены все три потребителя Telegram, а не только бот:
- tgbot и backend — extra_hosts в новом docker-compose.selectel.yml.
  backend тоже ходит в Telegram: пересылка алертов GlitchTip
  (api/v1/glitchtip.py) и support-чат (api/v1/support.py).
- хостовые скрипты ops/lib-backup.sh и ops/uptime-healthcheck.sh —
  шаг 11 в selectel-bootstrap.sh кладёт запись в /etc/hosts.
  Им compose не помогает, они идут из cron, не из контейнера.

Отдельный override-файл, а не правка docker-compose.prod.yml: на Beget
закрепление не нужно, и менять поведение действующего прода ради
будущего хоста нельзя. Файл просто не передаётся в -f.

Адрес вынесен в TELEGRAM_API_IP с текущим дефолтом — если он умрёт,
правка в одну переменную окружения без релиза. Шаг bootstrap проверяет
не факт записи, а что Bot API отвечает 401 на фиктивный токен, и при
другом коде печатает команду поиска нового живого адреса.

Проверено: docker compose config даёт ровно две вставки extra_hosts
(tradein-backend, tradein-tgbot) и ничего больше; TELEGRAM_API_IP
подставляется в обе.

Refs #3059, #3057
2026-08-23 23:32:35 +03:00
bot-backend
2eb7852622 chore(compose): параметризовать ресурсы postgres под переезд на новый хост
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Репетиция полного перелива на Poincare (12 потоков / 62 ГиБ / NVMe RAID1)
2026-08-23 вскрыла две мины окна 30.08, которых нет ни в одном issue.

1. mem_limit: 3g зашит жёстко. На 62 ГиБ целевое 44g, и лимит КОНТЕЙНЕРА
   обязан подниматься РАНЬШЕ shared_buffers — он срабатывает раньше
   postgresql.conf, поэтому shared_buffers=12GB при mem_limit=3g убивает
   контейнер OOM-kill'ом на старте. Предупреждение об этом в файле было,
   правки не было.

2. Оба compose монтируют каталог миграций в /docker-entrypoint-initdb.d.
   На СВЕЖЕМ томе цепочка из 249 миграций отработает ДО восстановления
   дампа и засеет данные (scrape_schedules 157 строк, tradein_users 13,
   deals 80), а дамп идёт с --clean --if-exists.

Параметризованы только те значения, что зависят от размера хоста:
TRADEIN_PG_MEM_LIMIT (одна переменная на mem_limit и memswap_limit —
инвариант «без свапа» обязан держаться и после правки), TRADEIN_PG_SHM_SIZE,
shared_buffers, effective_cache_size, work_mem, maintenance_work_mem,
max_wal_size, плюс TRADEIN_PG_INITDB_DIR / GENDESIGN_PG_INITDB_DIR.
checkpoint_timeout, wal_compression, random_page_cost, pg_stat_statements
от размера хоста не зависят — не тронуты.

Все дефолты равны текущим прод-значениям на Beget. Проверено docker compose
config дважды на обоих файлах: без переменных резолвится в 3g / 512m /
768MB / 6GB / 16MB / 256MB / 4GB и штатные пути initdb — бит-в-бит как
в main; с переменными — в 44g / 12GB / 36GB / 64MB / 2GB / 16GB.
Лимиты остальных сервисов не изменились.

Postgres Site Finder (корневой compose) намеренно получил только переменную
initdb: mem_limit и тюнинга у него сейчас нет вовсе, добавление изменило бы
поведение текущего прода. Он до сих пор на стоковом конфиге —
shared_buffers 128 МБ на базу 15 ГБ, wal_compression off — отдельная задача.

Refs #2989, #3057
2026-08-23 19:12:13 +03:00
bot-backend
fb38d657ad fix(tradein/deactivate-stale): пол переобхода — LATERAL вместо equality-join по дате
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 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 4m41s
Правка меняет две вещи разом, обе намеренно.

1. Равенство по дате -> «последняя строка не позже якоря».
   listing_source_snapshots переходит на модель «строка на изменение».
   В ней equality-join теряет 95.6% пар (на 2026-08-20 изменениями являются
   4458 строк из 101795): выборка для percentile_disc схлопывается до n=3-4,
   квантиль вырождается в максимум из трёх чисел, TTL обваливается —
   domklik 28->14 (под снятие сразу 128 активных строк), yandex 44->30,
   avito 13->10.
   Дыры в суточной истории ломали equality-join и до перехода: на проде
   03-14.06 (12 суток подряд), 03-04.07, 12.07, 26.07, 30-31.07, 01.08 —
   там n_pairs=0, floor_days=NULL и TTL молча оставался как задан.

2. Глобальный max(snapshot_date) -> максимум внутри источника.
   Старый подзапрос брал максимум по ВСЕЙ таблице, не скоупленный по
   listing_source_id: источник, чья история короче общей, выпадал из
   выборки целиком. LATERAL ищет предшественника по строке.

Замер на живом проде 2026-08-23 (health_window_days=3): обе формы дают
побитово одинаковый результат на всех четырёх источниках —
avito n=765 пол=49.7, cian n=1226 пол=81.6, domklik n=565 пол=35.7,
yandex n=3856 пол=87.3. Сегодня суточная джоба пишет строку для каждого
источника каждый день, поэтому глобальный максимум совпадает с максимумом
каждого источника, и пункт 2 — no-op на текущих данных. Расхождение
проявится только на дырах и после перехода на change-only.

Плюс floor_n_pairs в counters — наблюдательность для будущего гейта
деградации пола, сейчас ничего не блокирует.

FROM..WHERE вынесен в _revisit_floor_from_where_sql, чтобы count(*) и
percentile_disc гарантированно шли по одному срезу.
2026-08-23 15:45:53 +03:00
bot-backend
5b447ec33d feat(tradein/estimate): внешние оценки не ждутся в запросе — 9 секунд превращаются в 1
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 4m42s
Замер на проде 2026-08-22, разбивка одного расчёта по логам:

    10.659  старт
    10.827  дом найден              0.17 с
    11.565  аналоги, 49 кандидатов  0.74 с
    11.579  ДКП-коридор             0.01 с
    17.576  yandex_valuation        ← 6.0 с
    19.159  cian_valuation          ← 1.5 с
    19.252  готово                  итого 8.6 с

Семь с половиной секунд из восьми с половиной — ожидание чужих HTTP. Наша база
и сам расчёт укладываются в секунду. По уже виденному адресу (кэш 24 ч) — 0.4-0.8 с,
по новому — 8-12.4 с. Геокодинг ни при чём: с готовыми координатами те же 7.5-10.8.

Публикация в РБК 30.08 приведёт аудиторию на НОВЫЕ адреса, то есть мимо кэша.
Масштабирование контейнеров тут не помогает: время уходит на ожидание чужого
ответа, а не на наши вычисления.

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

Почему это безопасно: деградация источника в None — НЕ новое состояние ответа.
Ровно так же ведёт себя таймаут estimate_*_valuation_timeout_s, и этот путь
работает в проде сегодня. Контракт API не меняется.

Фоновая задача берёт СВОЮ сессию: сессия запроса закрывается вместе с ответом,
а обе функции источников делают внутри себя db.commit() — переиспользование
чужой сессии зафиксировало бы её незавершённую работу. По той же причине
отвергнут наивный asyncio.gather двух источников на одной сессии.

Очередь догрузки ограничена восемью задачами. Без потолка всплеск по новым
адресам — ровно тот случай, ради которого режим и сделан — породил бы сотни
параллельных задач с сессиями и HTTP-клиентами при max_connections 100 и
mem_limit 768m у backend, то есть отказ вместо ускорения.

Дефолт в коде False: поведение других окружений не меняется. На проде режим
включён через docker-compose.prod.yml у сервиса backend.

Отдельно НЕ сделано, хотя предлагалось: снижение таймаутов до 4 с. Замер
показал, что свежий запрос к Яндексу занимает 6 с — таймаут 4 обрывал бы его
почти всегда, кэш бы не наполнялся, и источник оказался бы тихо отключён.
Таймаут здесь страховка от патологии, а не регулятор задержки.

Тесты: 7 штук на режим «только кэш», собственную сессию, гашение ошибок,
удержание ссылки на задачу и потолок очереди. Фальсифицированы — на неизменённом
коде падают 5 из 7 (проходит только сторож неизменности дефолта). Смежные
тесты оценщика (29 штук: бюджет ЦИАН, клиентские координаты, аудит) зелёные.
2026-08-22 15:51:31 +03:00
bot-backend
fa1399d59e fix(tradein/avito): бэкфилл простаивал 23 часа из 24 — каденс был суточным
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 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 / backend-tests (pull_request) Successful in 4m47s
Замер 2026-08-22, уже после починки транспорта (#3049): обогащение работает,
но очередь не разбирается. За сутки 238 карточек при 9 951 активном объявлении.

Причина в планировщике, а не в скрапинге. `compute_next_run_at` имеет суточную
гранулярность по построению — `interval_days = max(1, int(...))`, целевая дата
`now + interval_days`. Меньше суток не выражается. Бэкфилл при этом умирает по
бану через 17-83 минуты, то есть работал около часа в сутки, а остальное время
расписание ждало следующего дня.

Механизм sub-hourly каденса уже был написан — `reschedule_after_minutes`
(#2162, сделан для proxy_healthcheck). Его просто не подключили к бэкфиллу.
Хук ставит next_run_at = now() + interval_minutes сразу после claim и сам себя
тормозит: пока прогон идёт, has_running_run в _claim_run возвращает None.

180 минут — осознанно консервативная отправная точка, НЕ найденный оптимум.
Данных для подбора нет, и имеющиеся два прогона противоречат наивному
ожиданию: 4562 дал 175 карточек за 83 минуты, а 4586 через 4.3 часа — когда
пул прокси был давно чист — умер за 17 минут с 42 карточками. Значит память
Авито длиннее часов, и учащение может ухудшить выход.

Отдельно держать в голове при подборе: те же 4 прокси обслуживают SERP-свипы,
то есть первичный сбор. Сжечь их на обогащении хуже, чем медленно обогащать.
Двигать интервал вниз только по замеру нескольких суток, глядя и на свипы.
Подбор — через default_params расписания, правка кода для этого не нужна.

Тесты закрепляют подключённость хука и коридор дефолта, а не конкретное
значение: 180 будет двигаться, а вот утрата хука вернёт суточный простой
молча. Проверено фальсификацией — на неизменённом коде все три падают.
2026-08-22 14:57:20 +03:00
bot-backend
b61123d7a6 fix(tradein/avito): detail_backfill ходил curl'ом, который не решает PoW-челлендж
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
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 / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m32s
Замер на проде 2026-08-21: Авито за QRATOR отдаёт JavaScript proof-of-work
челлендж (startPow → кука pow_solved → self-reload через window.location).
curl_cffi не исполняет JS и пройти его не может в принципе.

Что это давало в цифрах (counters прогонов avito_detail_backfill):

    run 4508: enriched 3  / attempted 46
    run 4394: enriched 2  / attempted 47
    run 4348: enriched 4  / attempted 53

~6% успеха. Браузерный путь на том же проде — ~74% (17/23 в инлайн-обогащении
свипа, 2/3 в точечной проверке после #3048). То есть задание работало почти
вхолостую, а поля дома, которые чинил #3048, до карточек просто не доходили.

Исходное обоснование AVITO_DETAIL_BACKFILL_USE_CURL=true (browser открывает
десятки коннектов и превышает cap прокси-аккаунта auv) больше не действует:
браузер сериализован BROWSER_CONCURRENCY=1 и ходит через тот же backconnect
SCRAPER_PROXY_URL, что и curl-путь.

Переключение сделано в docker-compose.prod.yml (environment перекрывает
env_file), а не правкой рантайм-env на хосте — чтобы изменение осталось в
репозитории и пережило deploy без дрейфа конфигурации. Дефолт в config.py
оставлен True: другие окружения не трогаем вслепую, но устаревшее обоснование
там помечено.
2026-08-22 00:11:59 +03:00