Commit graph

913 commits

Author SHA1 Message Date
990a59e469 fix(rbac): анониму на admin-префиксе отвечать 404, а не 401
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 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 5m6s
#3360. Периметр /trade-in/api/v1/admin/* снаружи не срезан (caddy/sites/apps.caddy:
блок `handle /trade-in/api/*` стоит выше `import caddy/users.caddy.snippet`), и
срезать его нельзя: admin-UI кабинета зовёт эти пути ИЗ БРАУЗЕРА (12 файлов
tradein-mvp/frontend/src — scrapers/**, components/scrapers/**, admin-audit-api.ts,
GuardedRoute). Значит внешний аноним доходит до rbac_guard, а после #3324
(несуществующий путь → 404 роутера) 401 на существующей ручке стал оракулом:
перебором имён восстанавливался список admin-API.

Все три ветки «личность не установлена» (нет X-Authenticated-User, auth_mode=db_only,
подделанный заголовок без #2213-секрета) на admin-префиксе теперь отдают ровно то же,
что роутер даёт на несуществующий путь. Аутентифицированные не тронуты: admin — 200,
не-admin — 403 «admin only» (прятать наличие ручки от опознанного человека незачем).

Смоук периметра: пара admin-путей — существующий /admin/proxies и несуществующий
/admin/users — оба обязаны быть 404 снаружи.
2026-09-06 00:51:09 +05:00
7afaa12d75 Merge pull request 'fix(tradein/avito): брейкер добора считает долю только по отказам площадки — сайдкар/пустой пул не рвут прогон и не красят его banned' (#3367) from fix/3288-breaker-block-kind 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 4m3s
Deploy Trade-In / build-backend (push) Successful in 1m5s
Deploy Trade-In / deploy (push) Failing after 32s
Deploy Trade-In / perimeter-smoke (push) Has been skipped
Deploy Trade-In / deploy-status (push) Failing after 1s
2026-09-05 19:38:48 +00:00
c82ec7c3af fix(tradein): понижать статус только у прогона, который работу сделал (#3288)
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 / 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 4m59s
Прошлая правка понижала banned→failed/done по одному диагнозу infra и ломала
обратный контракт: нулевой прогон с infra (yandex 5xx #3196, финализатор #2764)
получал 'failed' — настоящий бан площадки, опознанный как infra, прятался под
«нашу поломку». Хуже исходного дефекта: 2 красных теста в полном прогоне.

Понижение сужено до случая прогона 5425 — dominant='infra' И produced > 0:
брейкер оборвал по доле, а карточки при этом обогащались → 'done'. Нулевой
прогон остаётся 'banned' (честность несёт ban_kind), пустая перепись — тем
более: dominant='unknown', статус не трогаем.

Тесты: контроль на обратную ошибку (ноль результата → banned+infra) и на
пустой census (→ banned+unknown); основной кейс {'infra': 20} при 10
обогащённых — не banned.
2026-09-06 00:28:24 +05:00
6ced16618c Merge pull request 'fix(tradein/yandex): недорендеренная карточка — отказ, не успех: структурный маркер encryptedPhones + порог размера, карточка остаётся в очереди' (#3364) from fix/3191-yandex-detail-completeness 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 10s
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 4m2s
Deploy Trade-In / build-backend (push) Successful in 2m4s
Deploy Trade-In / deploy (push) Has been cancelled
2026-09-05 19:21:06 +00:00
78e3da6bc3 fix(tradein): маркер полноты — encryptedPhones ИЛИ redirectPhones (#3191)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
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 5m21s
Ревью #3364. Требуя именно encryptedPhones, отбраковывали бы вечно
необмеренный класс карточек «без телефона / только чат»: в очередь они
возвращаются, а ключ не появится. redirectPhones измерен тем же замером
#3192 и присутствует в обеих ветках (с куками и без).

Текст ABORT: consecutive_none смешанный (фетч-ошибка + parse-None +
недогруз) — «N подряд без обогащения», а не «недогруженных».
2026-09-06 00:14:43 +05:00
ec8213c269 fix(payments): обрыв соединения и Init без PaymentURL перестают запирать покупателя
All checks were successful
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 / 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 5m11s
Три дыры в одном замке (строка без payment_url невидима для _find_live_payment,
но видима предикату UNIQUE 279 → ложный 409 на 30 минут):

- tbank_client ловил пару (TimeoutException, NetworkError): RemoteProtocolError,
  ProxyError и UnsupportedProtocol летели наружу голым httpx-типом мимо
  `except TBankApiError` в checkout. Ловим родителя — httpx.TransportError.
- Ветка «Success:true без PaymentURL» отвечала 502, не трогая статус, — здесь
  банк заказ ПРИНЯЛ. Тот же терминальный статус, error_code=no_payment_url;
  UPDATE вынесен в общий _mark_init_failed.
- _FakeDb в тестах применял статус по наличию ключа в params, а не по тексту
  SQL: мутант без `SET status = :status` оставался зелёным. Гейт по SQL —
  мутант краснит все три теста про замок.

Комментарий про «возможный холд» на ветке отказа Init поправлен: Init холд не
создаёт, авторизация идёт с оплаты формы, а форму покупателю не выдавали.
2026-09-06 00:11:35 +05:00
1a693192e1 fix(tradein): вид блока решает обрыв по доле и статус прогона (#3288 часть B)
Some checks failed
CI Trade-In / changes (pull_request) Successful in 7s
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 / openapi-codegen-check (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Failing after 5m19s
Прогон 5425 оборвался по доле блоков и получил статус banned на 48 «блоках»,
из которых 41 был отказом нашего сайдкара: record_block() вида не принимал,
поэтому infra падал в числитель скользящего окна #3184 наравне с настоящим
баном, а mark_backfill_finished считал диагноз только ради телеметрии.

- record_block(kind): в числитель идёт только platform; всё остальное — в
  знаменатель (как record_failure), мимо серии и safety-net;
- NoProxyAvailableError у avito — не блок и не отказ площадки: прогон
  завершается no_proxy_stop=1 + mark_failed «пул прокси пуст» (как домклик
  после #3283); опознаётся по цепочке __cause__, не по подстроке (#3272);
- статус banned — только при доминировании platform; при infra прогон
  получает failed (нулевой результат) или done, с честной причиной.
2026-09-06 00:09:37 +05:00
4b5f8fb54c fix(tradein): недогруженная карточка Яндекса — отказ, а не успех (#3191)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
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 / openapi-codegen-check (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m11s
Страница на 1,8 МБ без блока контактов приходит с HTTP 200 и валидным HTML:
window.INITIAL_STATE на месте, parse отрабатывает — и частичная карточка уезжала
в БД с detail_enriched_at, выбывая из очереди навсегда. Единственная проверка
размера (newbuilding.py, len(html) < 500) отвечала на вопрос «пришло ли хоть
что-то»: 1,8 МБ проходит её в 3600 раз.

Признак полноты структурный + размерный, любой из двух даёт отказ:
encryptedPhones (65 вхождений у полных карточек, 0 у недогруза; отдаётся и
анонимной сессии — см. yandex_session.py) и settings.yandex_detail_min_html_bytes
(1 МБ). Наблюдавшийся недогруз ловит именно структурный: 1,8 МБ порог проходит.

В backfill проверка стоит ДО parse: исход incomplete ⊆ failed, save не
вызывается, значит detail_enriched_at не проставляется и следующий снапшот
(detail_enriched_at IS NULL) возьмёт объявление снова. Серия недогрузов двигает
consecutive_none — тот же брейкер, что у parse→None, поэтому вечно недогружаемая
карточка обрывает прогон, а не молотится (per-listing счётчика попыток в схеме
нет).

Фейковые ответы в тестах-соседях (#3196/#3338) теперь при HTTP 200 выглядят
полной страницей — иначе они молча стали бы кейсами про полноту.
2026-09-05 23:59:15 +05:00
5bd9c586b7 fix(payments): отказ банка на Init больше не запирает покупателя на 30 минут
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
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 4m52s
Строка после провального Init оставалась в NEW и без payment_url: для
_find_live_payment (фильтр payment_url IS NOT NULL) её нет, а для предиката
UNIQUE миграции 279 — есть. Следующий checkout ловил конфликт и получал 409
'retry shortly' до истечения _ABANDONED_AFTER_MINUTES — из-за сбоя банка, а
не своего действия.

Переводим такую строку в терминальный DEADLINE_EXPIRED (тот же, которым
checkout уже помечает брошенные попытки) в том же UPDATE, что пишет
error_code/error_message: статус вне предиката 279, пара
(estimate_id, product_code) освобождается сразу. Новый статус в CHECK 233 не
заводим — миграция ради ярлыка не нужна, причина и так в error_*.

Refs #3323
2026-09-05 23:54:22 +05:00
bab4b4f8ff Merge pull request 'fix(tradein/rbac): 401-оракул закрыт — несуществующий путь под публичным префиксом отвечает так же, как любой другой мусор' (#3352) from fix/3324-rbac-401-oracle 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 15s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 2m28s
Deploy Trade-In / test (push) Has been cancelled
2026-09-05 18:19:27 +00:00
65155e5ccd Merge pull request 'fix(tradein/backfill): save-False у yandex/avito больше не теряет попытку — attempted == сумма исходов у всех трёх бэкфиллов' (#3347) from fix/3338-backfill-save-false into main
Some checks failed
Deploy Trade-In / changes (push) Successful in 16s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Has been cancelled
Deploy Trade-In / test (push) Has been cancelled
Deploy Trade-In / build-backend (push) Has been cancelled
Deploy Trade-In / deploy (push) Has been cancelled
Deploy Trade-In / perimeter-smoke (push) Has been cancelled
Deploy Trade-In / deploy-status (push) Has been cancelled
2026-09-05 18:14:06 +00:00
b8084f6afe Merge pull request 'fix(tradein): секрет вебхука GlitchTip не попадает в access-log — скруббер query-секретов + приём из заголовка' (#3353) from fix/3154-glitchtip-secret-log-leak into main
Some checks are pending
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) Successful in 24s
2026-09-05 18:12:58 +00:00
8c4be70193 fix(rbac): честный докстринг про размен оракулов + трейлинг-слэш больше не выдаёт маршрут
All checks were successful
CI Trade-In / changes (pull_request) Successful in 16s
CI / changes (pull_request) Successful in 19s
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 5m51s
Ревью #3352: комментарий у _path_is_routed утверждал «ослабления нет» — неверно.
Раньше 401 был ПРЕФИКСНЫМ оракулом (таблицу маршрутов по нему не перечислить),
теперь 401/404 — оракул СУЩЕСТВОВАНИЯ маршрута, включая имена admin-ручек.
Докстринг переписан честно, с проверенным по caddy/sites/apps.caddy фактом:
блок handle /trade-in/api/* стоит выше import users.caddy.snippet, внешнего
basic_auth у trade-in нет — значит перебор имён выполним и снаружи.

Второй канал того же оракула закрыт: /api/v1/me/ не матчил ни один маршрут,
guard пропускал, а роутер отвечал 307 на существующий путь. FastAPI получил
redirect_slashes=False (проверено: ни одного route с трейлинг-слэшем, ни одного
такого вызова во фронте; deny-правила уже на глоб-форме).

Тесты: PARTIAL-кейс (POST на GET-путь анониму → 401, не 404/405), трейлинг-слэш
на РЕАЛЬНОМ app.main (тест на копии был бы тавтологией), admin-гейт сверяется по
тексту 'admin only' — scope-ветка отвечает тем же 403, но 'forbidden for role'.
2026-09-05 23:12:17 +05:00
659543f004 fix(tradein/backfill): warning save-False называет оба id (avito) и не дублирует listing_id (yandex)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 15s
CI / changes (pull_request) Successful in 17s
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 6m13s
Ревью #3347, два minor.

1. avito: WHERE в save_detail_enrichment ключуется по source_id из РАЗОБРАННОГО
   HTML (enrichment.item_id), а warning печатал row["id"] из снимка. При
   редиректе/подмене карточки строка с row-id жива, и читатель лога идёт искать
   несуществующую проблему не у той строки («текст ошибки называет гонца»).
   Печатаем оба: listing_id=%s item_id=%s.
2. yandex: listing_id стоял в строке дважды (в начале и как «id=%d» в хвосте) —
   убран дубль.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 23:06:22 +05:00
55a67e5c45 fix(tradein): маскировать секрет и в параметрах с префиксом (#3154)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 12s
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 5m38s
Ревью-minor: якорь [?&] вплотную к имени пропускал client_secret=,
refresh_token=, auth_token=, webhook_secret=. Разрешаем префикс [\w.-]*
перед альтернацией — маскируем имена, ОКАНЧИВАЮЩИЕСЯ на чувствительное
слово. Кейс not_a_secret_name заменён на честные отрицательные:
?secretary= и ?tokens_page= (не оканчиваются на secret/token) плюс
/api/v1/token-info в пути.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 23:03:16 +05:00
ed94a03f73 fix(tradein): не логировать секрет вебхука GlitchTip в access-log (#3154)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 12s
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 5m9s
uvicorn печатает в access-log полный путь вместе с query, поэтому секрет
вебхука (`?secret=`, он же TRADEIN_INTERNAL_AUTH_SECRET, второй рубеж rbac)
уезжал в Loki открытым текстом. Скруббер #3115 в Alloy ловит только форму
`user:pass@host` (DSN postgres_exporter) и такую строку не закрывает.

Два слоя:
- хендлер принимает секрет из заголовка X-GlitchTip-Secret; query-параметр
  остаётся fallback'ом, т.к. сам GlitchTip 6.1.6 (`send_webhook()`)
  заголовков не шлёт вовсе — убрать query можно только когда заголовок начнёт
  подставлять кто-то перед нами (Caddy header_up) или сменится отправитель;
- app/core/log_scrub.py: logging-фильтр маскирует значения чувствительных
  query-параметров (secret/token/api_key/…) на uvicorn.access и на
  обработчиках корневого логгера — секрета нет уже в `docker logs`.

Сравнение секрета и было constant-time (`secrets.compare_digest`).
2026-09-05 22:51:57 +05:00
0ef1880e55 fix(rbac): не отвечать 401 за несуществующий путь (#3324)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 15s
CI / changes (pull_request) Successful in 18s
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 5m36s
rbac_guard — HTTP-middleware, он отрабатывает до роутинга и потому отвечал
401 с rbac-текстом даже на пути, которых в приложении нет. Аноним получал
бесплатный оракул периметра: мусор под «интересным» префиксом давал 401, а
такой же мусор под публичным префиксом — 404 роутера, то есть выключенная
ручка была отличима от несуществующей.

Guard теперь пропускает запрос дальше, если ни один маршрут роутера не
матчится (Match.NONE у всех) — 404 отдаёт тот же роутер, что и на любой
другой мусор. Существующие маршруты не затронуты: Match.PARTIAL (путь есть,
метод другой) по-прежнему идёт в guard, реальный закрытый маршрут анониму
даёт 401, публичный — работает без идентичности.
2026-09-05 22:51:22 +05:00
3658434c03 fix(tradein/backfill): save-False у yandex/avito больше не теряет попытку — attempted == сумма исходов
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 12s
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 5m41s
Тот же дефект, что #3332 у domclick (#3335), у обоих соседей: `if save(...):
enriched += 1` без else. UPDATE, не задевший строку (объявление удалено между
снимком и записью), оставлял попытку без исхода — attempted переставал сходиться
с суммой исходов, и расхождение читается как потерянный отказ площадки.

Разбор ВСЕХ точек выхода из цикла попыток показал, что у соседей это
единственная дыра: обрыва «пул прокси пуст» у них нет (resolve_proxy_url
бросает ProxyPoolExhaustedError ДО цикла), а budget/SIGTERM-брейки стоят до
`attempted += 1`. Исход — failed с отдельным warning про ненайденную строку.

Формы тождества разные и это не описка: у yandex blocked ⊆ failed (#3196),
у avito blocked/gone/failed — непересекающиеся корзины.

Тесты — по значению (числа, не «не бросило»), плюс контроль на противоположную
ошибку (исход не начисляется дважды). В test_3332 добавлен запрошенный ревью
кейс: транспортный сбой → пустой пул даёт attempted=2 failed=2, а не 3.

Closes #3338
2026-09-05 22:47:08 +05:00
aaf8119408 Merge remote-tracking branch 'origin/main' into HEAD
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 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 / backend-tests (pull_request) Successful in 5m1s
# Conflicts:
#	tradein-mvp/backend/app/services/estimator.py
2026-09-05 22:40:52 +05:00
99f112db0b Merge pull request 'fix(tradein/auth): единый источник ролей — БД first, YAML только legacy-fallback (закрывает эскалацию до admin и 403 своим)' (#3331) from fix/3316-role-single-source into main
All checks were successful
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 4m5s
Deploy Trade-In / build-backend (push) Successful in 1m47s
Deploy Trade-In / deploy (push) Successful in 2m8s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 13s
2026-09-05 17:37:38 +00:00
126608e1fc Merge pull request 'fix(tradein/domclick): обрыв «пул пуст» и save-False больше не теряют попытку — attempted == сумма исходов' (#3335) from fix/3332-domclick-counter-identity 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 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-09-05 17:36:54 +00:00
bot-backend
76b24e9f4d fix(mera-public): подписи метрик снижения цены и бенчмарк отчёта не называют площадку
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 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 / frontend-checks (pull_request) Successful in 1m12s
CI Trade-In / backend-tests (pull_request) Successful in 5m5s
Замечание юриста (Сайт_МЕРА_v2, п.3): публичный измеримый контент не должен
строиться на названных данных площадок.

- landing_stats.py: note метрик price_cut_share_pct / price_cut_median_pct_per_month
  «Только Домклик (…)» → «Только одна из площадок (…)». Текст персистится в
  landing_stats.note и рендерится на лендинге в блоке «цена ошибки» — после
  деплоя нужен пересчёт (refresh_landing_stats), деплой кода note не перепишет.
- HeroSummary.tsx / IMVBenchmark.tsx: убраны видимые «IMV» и title
  «Information Market Value» / «Индекс Market Value» — бренд-термин оценочной
  модели площадки; подпись — «оценочная модель» + sourcePublicLabel.
- public-copy-no-platform-names.test.ts: «IMV», «Information Market Value»
  добавлены в BANNED (гейт 34/34).
2026-09-04 11:27:32 +03:00
bot-backend
f2f9ac941b fix(tradein/pdf): клиентский PDF не называет площадки-источники — «Источник N», как в веб-отчёте (#3341)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 5m7s
Веб-отчёт и лендинг с #3342 не показывают названия площадок (канон publicLabel в
frontend/src/lib/source-registry.ts), а клиентский PDF по тому же /estimate/{id}
печатал Avito / Циан / «Домклик · Сбер» / Я.Недвижимость / Этажи в пилюлях
источников, «подтверждают Росреестр, ДомКлик…» в советах и «на Циан, Авито,
Я.Недвижимости» в тарифах. Один клиент — два документа с разной нормой, и юр-риск,
ради которого всё делалось, в PDF оставался открытым.

- `_SOURCE_DISPLAY_NAMES` → публичные лейблы 1:1 с реестром фронта: avito → «Источник 1»,
  cian → 2, yandex → 3, domklik → 4, etazhi → 5, rosreestr → «Росреестр»; fallback для
  незнакомого id — «Другой источник», а не `source.title()` (сырой id — та же утечка).
- Алиасы (avito_imv, cian_valuation, yandex_valuation, domclick, etagi) канонизируются
  ДО выбора цвета и лейбла (`_canonical_source`), а списки источников на страницах
  объявлений и сделок дедуплицируются до среза [:5] (`_public_sources`): иначе
  `sources_used` = listing ∪ valuation давал «Источник 1, Источник 1, Источник 2,
  Источник 2, Источник 4» с серой точкой у алиасов и вытеснял yandex.
- Цвета пилюль не тронуты: цвет — опознаватель источника, как на вебе.
- Тексты: «Росреестр, сделки площадок и продажи агентств», «на основных площадках
  объявлений» (двойник offer-rates.ts).
- Гейт `tests/test_pdf_public_source_labels.py`: видимый текст страниц (без тегов и
  атрибутов, href на домены площадок законны) не содержит названий площадок и сырых
  id, по одному кейсу на имя; дедуп алиасов; ветка совета с процентом.

Не тронуто: ссылки на объявления (avito.ru/domclick.ru) — отдельное решение;
`_QUALITY_SOURCE_SLOTS` — только счётчик, имена не рендерит.
2026-09-03 11:58:45 +03:00
bot-backend
5541aa9d37 fix(mera-public): публичная копия — сервис не называет себя оценщиком, площадки-источники не названы
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 / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m7s
CI Trade-In / backend-tests (pull_request) Successful in 5m2s
Три юр-правки по документу владельца продукта «Сайт_МЕРА_v2» (31.08.2026), сделаны
локально 31.08, но не были закоммичены — на meraocenka.ru всё оставалось по-старому.

1. «Оценщик» про собственный алгоритм. Витрина сделок (`landing_showcase_deals.py`,
   REJECTION_RULE/NOTE) и сноска статьи «Как оценить квартиру» теперь говорят
   «расчёт МЕРЫ»: самоназвание обесценивало дисклеймер «не официальный отчёт оценщика».
   Комментарии и докстринги не тронуты — посетитель их не видит.
2. «Путь 2»: «стоимость услуг фиксированная и известна заранее» — читалось как
   фиксированная цена квартиры.
3. Названия площадок убраны из видимой копии лендинга и веб-отчёта. Канон —
   `publicLabel` + `sourcePublicLabel()` в `source-registry.ts`: одна площадка = один
   номер «Источник N» и один цвет точки (цвет остаётся опознавателем между блоками),
   Росреестр под своим именем, неизвестный id → «Другой источник» (раньше fallback
   отдавал сырой id). Три параллельные реализации `sourceLabel` сведены к одной;
   `sourceLabel()` с реальными именами живёт для админки. Backend: якорь
   confidence_explanation «по оценке Avito IMV» → «по оценочной модели площадки».
   Подсказки геокодера «Yandex / Nominatim» и «по Яндексу» сняты из клиентских форм.

Гейт `public-copy-no-platform-names.test.ts` сканирует mera-public/** и
components/trade-in/** без комментариев, по Unicode-границе слова; список запретов
регистрозависимый намеренно (строчные id `"avito"` законны), поэтому капс-варианты
и словоформы перечислены явно — «6202 ОБЪЯВЛЕНИЯ ДОМКЛИК» в статье именно так
проходил первую версию гейта.

Не закрыто здесь: клиентский PDF (#3341) и ссылки на объявления на доменах площадок.

Текст витрины хранится в БД (`landing_showcase_runs.rejection_rule`,
`landing_showcase_deals.note`), планировщика у пересчёта нет — после деплоя нужен
ручной пересчёт или UPDATE трёх подстрок на проде.
2026-09-03 09:50:41 +03:00
4f4345e27c fix(tradein): завершить thin-market гейт IMV — Guard-1b и GET-путь (#3323)
All checks were successful
CI Trade-In / 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 / 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 5m23s
MEDIUM-1: imv_anchor_present ставился по anchor_total МИМО гейта — на тонком
рынке якорь отброшен, а Guard-1b (#764) продолжал глушить квартальную поправку
«потому что якорь есть»: headline не получал ни одной поправки, отброшенный
якорь двигал деньги вычитанием. Теперь present = not thin_market.

MEDIUM-2: trade_in.py (GET ?id= — расшаренная ссылка/PDF) — третья точка
сборки карточки: market_count=0 читался как «неизвестно», thin_market не
передавался вовсе → одна оценка показывала thin_market=True в POST и False
при переоткрытии.
2026-09-02 17:44:32 +05:00
148da3e5a7 fix(tradein): thin-market IMV не двигает деньги (#3323)
All checks were successful
CI Trade-In / 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 / 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 5m6s
avito_imv_thin_market_threshold рождал только warning: IMV с market_count=1
всё равно уходил в blend (w=0.5 при A > median*1.15) и растягивал range_high.
Гейт поставлен в _apply_imv_blend — единственной точке, через которую IMV
влияет на деньги (обе ветки якоря, imv_anchor и imv_eval, сходятся там):
market_count < threshold → no-op, якорь остаётся display-only в карточке.
market_count >= threshold и market_count=None (порог не передан) — поведение
прежнее. market_count=0 больше не читается как «неизвестно».
Warning теперь говорит, что IMV ОТБРОШЕН, а не просто «тонкий рынок».
2026-09-02 17:36:22 +05:00
e2e7e17f95 fix(domclick): каждая попытка добора получает исход (#3332)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
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 / changes (pull_request) Successful in 12s
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 5m15s
Обрыв «пул прокси пуст» уходил из цикла между attempted++ и записью исхода,
поэтому тождество attempted = enriched + failed + blocked ломалось ровно на 1
(прод: 5 прогонов с diff=1). Исход честно failed, не blocked: к площадке не
ходили, отказала наша инфраструктура — тот же разряд, что у транспортных сбоев
(#3283); причина прогона по-прежнему в no_proxy_stop=1 + mark_failed.

Та же дыра закрыта у save_detail_enrichment(...) is False: карточка разобрана,
но строки уже нет — попытка была, исхода не было.

Closes #3332
2026-09-02 17:35:48 +05:00
cde95aa9f6 test(tradein): прод-конфигурация ролей в приёмке #3316 + компромисс фолбэка
Some checks failed
CI Trade-In / changes (pull_request) Successful in 10s
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) Failing after 5m8s
Review PR #3331: приёмка «роли не изменились» гонялась с ПУСТЫМ реестром, а в
проде строка в БД есть у 12 из 13 юзеров и DB-роль ИНАЯ (kopylov: manager при
YAML pilot, user1: employee при YAML pilot). Добавлены два кейса именно этой
конфигурации:

  * YAML pilot + реестр employee → employee, и scope не поехал: allow/deny
    DB_ROLE_PATHS['employee'] сверяются со списками роли pilot из roles.yaml
    целиком — дрейф ЛЮБОГО из двух списков теперь красный тест, а не тихо
    потерянный/выданный раздел в проде;
  * YAML pilot + реестр manager → manager, и лишних путей на tradein-периметре
    нет: manager отличается от employee ровно префиксом /api/v1/team/** (вне
    /trade-in/**), deny-списки совпадают.

Докстринг `_registry_role`: зафиксирован компромисс — при недоступном реестре
фолбэк временно возвращает авторитетность roles.yaml, то есть состояние, которое
фикс и лечит. Сегодня безопасно (прод-коллизий имён нет, новые закрыты
409-гвардом create_employee); появится коллизия — ветку менять на fail-closed.
2026-09-02 14:59:14 +05:00
4feb61c006 fix(tradein): резолвить роль из реестра, roles.yaml — только fallback (#3316)
Some checks failed
CI Trade-In / changes (pull_request) Successful in 12s
CI / changes (pull_request) Successful in 15s
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) Failing after 5m27s
Роль жила в двух местах сразу: люди заводятся в БД (`tradein_users.role`),
а `get_role` читал ТОЛЬКО `auth/roles.yaml` — и никто эти два источника не
сверял. Дефект двусторонний:

  * вверх: менеджер заводил сотрудника с именем, которое уже числится в
    roles.yaml админом (проверялись лишь regex и уникальность в БД) — на
    входе тот получал admin из YAML, то есть чтение ЛЮБОЙ чужой оценки
    (admin проходит мимо ownership-check в trade_in.py) и безлимитную квоту;
  * вниз: сотрудник, которого в roles.yaml нет, ловил KeyError → 403 на
    СОБСТВЕННУЮ оценку.

Источник теперь один и лечится один раз — в `app.core.auth.get_role`:
реестр (`tradein_users.role` / `auth.users.role`) спрашивается первым,
roles.yaml остаётся fallback для legacy-юзеров, у которых строки в реестре
нет. Реестр недоступен → тоже fallback: падение БД не выключает legacy-вход.
Вызывающие (rbac, trade_in, team, account_quota) не менялись.

Сопутствующее, чтобы поведение существующих аккаунтов не поехало:
  * rbac_guard выбирает матчер путей по РОДУ роли (роль реестра → DB_ROLE_PATHS),
    иначе employee/manager на legacy-пути получил бы 403 на всё;
  * get_user_scope отдаёт scope роли реестра из того же DB_ROLE_PATHS;
  * право на персональный `unlimited` осталось за roles.yaml (account_quota +
    _batch_quota_status) — фикс убирает эскалацию, а не раздаёт новую;
  * `_batch_quota_status` берёт роли из уже прочитанных строк — иначе список
    «Команды» снова стал бы N+1.

Defense-in-depth: create_employee отдаёт 409 на username, за которым в
roles.yaml числится не-employee роль.
2026-09-02 14:49:27 +05:00
add974db65 feat(tradein/avito): бан площадки не менял IP — прогон добивался в тот же забаненный адрес (#3283) (#3313)
All checks were successful
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) Successful in 4m10s
Deploy Trade-In / build-backend (push) Successful in 1m7s
Deploy Trade-In / deploy (push) Successful in 7m47s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 13s
2026-09-01 08:05:48 +00:00
bot-backend
d708f15019 fix(tradein/support): один повтор терял каждое одиннадцатое сообщение в поддержку
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 4m50s
Замер прода 01.09.2026 из контейнера бота: канал до api.telegram.org рвётся
всплесками, доля отказов на попытку 15-38% (пять проб: 3/8, 15/40, 5/20, 3/20,
1/25), в логе long-polling'а 353 ConnectTimeout за сутки. Транспорт ни при чём —
httpx и сырой сокет отваливаются одинаково (25% против 35% в чередующемся
замере), и прокси не помогает, а мешает: через SCRAPER_PROXY_URL 0 из 20.

Ручка веб-поддержки ходила с max_retries=1, то есть двумя попытками. При 30%
отказов на попытку до пользователя доходило ~9% отказов — каждое одиннадцатое
сообщение возвращало 502 «сервис недоступен».

Два других числа из того же замера задают конструкцию. Успешный запрос отвечает
за 0.13с (максимум из 25 проб — 0.18с), а неудачный НИКОГДА не отваливается
быстро: все отказы упираются в таймаут целиком (10.02с при timeout=10.0). Значит
десятисекундный таймаут не покупал ничего, кроме цены за неудачу, — снижен до 5с,
это ~28-кратный запас к измеренному максимуму. И экспоненциальная пауза 2→4→8с
здесь бессмысленна: отказ — неустановленное соединение, а не троттлинг, пережидать
нечего; она лишь добавляла 14с к ожиданию.

Правка: бюджет ручки — 3 повтора, таймаут 5с, потолок паузы 1с. Худший случай
4 попытки × 5с + 3 паузы × 1с = 23с и требует четырёх отказов подряд; типичный
случай не меняется (0.13с). Расчётная потеря падает с ~9% до ~0.8%.

В TelegramClient добавлен необязательный max_backoff. Воркерная политика НЕ
меняется: без явного потолка откат прежний экспоненциальный до 30с, а retry_after
из 429 уважается целиком — эту границу держит отдельный тест, потому что первая
версия правки её сломала (капала 60с до 30с и для воркера тоже). Потолок на
retry_after применяется только когда его передали явно: интерактивному пути
нельзя ждать Telegram-овские 30-60с, за ним стоит открытый запрос от браузера.

Тесты: 8 новых (потолок на network/429/5xx, неизменность воркерного пути,
арифметика «max_retries=N → N+1 попыток», границы бюджета ручки).
2026-09-01 09:53:21 +03:00
4c29b59041 fix(tradein/proxy): успешный fetch затирал exit_ip и latency_ms в NULL (#3283) (#3305)
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
Co-authored-by: lekss361 <lekss361@gendsgn.local>
Co-committed-by: lekss361 <lekss361@gendsgn.local>
2026-08-31 14:53:33 +00:00
9219ab6507 feat(tradein/avito): ротация exit-IP по счётчику попыток в доборе карточек (#3302)
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) Has been skipped
Deploy Trade-In / test (push) Successful in 4m8s
Deploy Trade-In / build-backend (push) Successful in 1m56s
Deploy Trade-In / deploy (push) Successful in 1m43s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
2026-08-31 12:09:03 +00:00
b7c1c4d9d4 fix(tradein/proxy_pool): истёкший прокси больше не выдаётся в аренду (#3303)
All checks were successful
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) Successful in 4m14s
Deploy Trade-In / build-backend (push) Successful in 1m16s
Deploy Trade-In / deploy (push) Successful in 7m32s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 12s
2026-08-31 11:54:54 +00:00
ea8943e00e Merge pull request 'chore(tradein/scheduler): добор карточек Яндекса и Циана шёл раз в сутки и простаивал' (#3301) from chore/cadence-yandex-cian-detail into main
All checks were successful
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) Successful in 4m17s
Deploy Trade-In / build-backend (push) Successful in 1m48s
Deploy Trade-In / deploy (push) Successful in 1m40s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 12s
2026-08-31 11:36:24 +00:00
bot-backend
4ddb3c7196 chore(tradein/scheduler): добор карточек Яндекса и Циана шёл раз в сутки и простаивал
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
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 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 4m58s
compute_next_run_at держит суточную гранулярность (interval_days, минимум 1),
подчасовой такт делается хуком reschedule_after_minutes как post_claim. У
avito_detail_backfill он есть (180 мин), у yandex_detail_backfill и
cian_detail_backfill не было — отсюда один прогон в сутки.

Цена простоя по замеру прода 31.08:

  Яндекс: 375-450 карточек за прогон, блоков НОЛЬ за неделю, очередь 11110
          → 25 суток при нынешнем такте
  Циан:   блоков ноль, очередь 20501

Такты разные, и это не произвол:

  yandex — 180 мин (8 прогонов/сутки). Ходит через resolve_proxy_url: берёт
           URL узла, но НЕ лизует его, поэтому чужие прогоны не блокирует.
  cian   — 360 мин (4 прогона/сутки). Ходит через BrowserFetcher и ДЕРЖИТ
           lease весь прогон, то есть отнимает узел у Авито и Домклика. Пул
           дефицитен (#2638), поэтому осторожнее.

Асимметрия зафиксирована комментарием у обоих хендлеров и в докстринге
миграции — иначе следующий читатель выровняет интервалы и сожжёт пул. Тест
test_cian_default_interval_is_360_minutes_not_180 ассертит именно неравенство,
чтобы выравнивание без замера покраснело.

283_scrape_schedules_cadence_yandex_cian_detail.sql — идемпотентный
UPDATE ... SET default_params = default_params || jsonb, остальные ключи
параметров не трогает.

Значения 180/360 — консервативная отправная точка по аналогии с Авито, а не
найденный оптимум: двигать вниз только по замеру нескольких суток, глядя и на
свипы тоже (тот же довод, что в комментарии у avito_detail_backfill).

Тесты (5): наличие post_claim у обоих, дефолтные интервалы, переопределение
через params. Прогон: 397 passed, 1 skipped, ruff чист.
2026-08-31 14:17:12 +03:00
a256077211 feat(tradein/proxy_rotation): ротация IP через mobileproxy.space (aproxy.site) (#3298)
All checks were successful
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) Successful in 4m11s
Deploy Trade-In / build-backend (push) Successful in 1m5s
Deploy Trade-In / deploy (push) Successful in 7m37s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 13s
2026-08-31 11:01:41 +00:00
e45b05845e fix(mera-landing): наличие схемы улицы входит в полноту строки витрины
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 13s
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 5m14s
Первой строкой витрины и первым раундом игры «Проверьте себя» стояла
единственная из двадцати сделка БЕЗ улицы: у неё вместо схемы улиц
рисовался полигон района. Причина — `completeness()` считала район, этаж
и этажность, но не улицу, хотя именно она решает, будет ли у строки карта.

Схема улицы — такое же ВИДИМОЕ поле, как район: строка, которой нечем
нарисовать карту, полнее строки с картой быть не может. Признак берётся
из уже загружаемого индекса улиц (`StreetIndex.lookup`, поиск в памяти);
индекс поднят выше отбора, дорогие пространственные запросы остались в
`_schemes_for` и по-прежнему считаются только для показанных строк.

Отбор по ВЕЛИЧИНЕ ОШИБКИ не введён и введён быть не может: строка
с отклонением +75,7% остаётся в витрине, просто больше не открывает её.
Прежнее правило «наличие схемы на отбор не влияет» в докстринге
`_schemes_for` заменено с разбором, почему оно давало этот дефект.

Тест двусторонний: при прочих равных строка со схемой выше строки без
неё, а строка без улицы остаётся в витрине. Фальсифицирован — снятие
`row.has_street` из `completeness` даёт красное ПО ЗНАЧЕНИЮ ([9, 8]
вместо [8, 9]), не по исключению.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 11:22:39 +05:00
bot-backend
1e02badbe9 fix(tradein/domclick): бан сайдкара наследуется от httpx-ошибки и уезжал в транспортные сбои (#3283)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
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 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 Trade-In / backend-tests (pull_request) Successful in 5m5s
Правка #3286 разделила отказы площадки и сбои нашей стороны по природе
исключения: httpx-ошибка в цепочке причин = транспорт. Прод показал дыру
на первом же прогоне (5406):

  СБОЙ #1/400 (подряд=1/10, http=401, kind=platform, не блок площадки):
  DomClick detail: sidecar detected platform refusal

Заголовок «не блок площадки» стоит над записью, где kind=platform и код 401,
то есть над самым что ни на есть отказом площадки. Причина в иерархии:
SidecarBanPageError объявлен как подкласс httpx.HTTPStatusError, поэтому
проверка на httpx, стоявшая первой, забирала генуинный бан себе.

Порядок проверок перевёрнут: бан сайдкара отсекается ДО общей httpx-ветки.
Остальное поведение #3286 не тронуто — обычная 500 от сайдкара по-прежнему
транспортный сбой.

Тесты: +4, включая закрепление самой предпосылки (issubclass проверяется
явно, чтобы смена иерархии не отключила проверку молча) и страховку, что
обычная 500 осталась сбоем. Проверено мутацией: снятие порядка роняет тест
и печатает самопротиворечивый лог «10 сбоев подряд без единого отказа
площадки, диагнозы: {'platform': 10}». Набор целиком — 5210 passed.
2026-08-30 17:02:46 +03:00
bot-backend
42913d2843 fix(tradein/domclick): таймаут сайдкара и пустой пул считались отказами площадки (#3283)
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 4m56s
Прогон 5399 умер по правилу «3 блока подряд», не обогатив ни одной карточки
из 400. Отказом Домклика не был ни один из трёх: два — HTTP 500 от сайдкара
(таймаут навигации), третий — пустой пул, при котором запрос к площадке не
отправлялся вовсе. Лог абортa сам это печатал: диагнозы {'unknown': 3}.

Различать по HTTP-статусу нельзя: настоящий QRATOR-челлендж приходит без
статуса и неотличим от таймаута — на этом и стояли прежние тесты. Различима
природа исключения, причём разделение уже проведено в fetch_detail: генуинный
бан приезжает через SidecarBanPageError или маркеры в parse_detail_html, а
транспорт — через  с , то есть с
httpx-ошибкой в цепочке причин.

Три правки:
* пустой пул (NoProxyAvailableError в цепочке) — не блок, а остановка прогона:
  продолжать бессмысленно, каждая следующая карточка упрётся в то же. Прогон
  уходит в failed, а не banned — запись не должна утверждать про площадку то,
  чего не было;
* транспортные сбои идут в failed, туда же, куда давно идёт DomClickParseError
  («schema drift, not a block»), и счётчик блоков не трогают;
* отдельный сторож на молчаливый отказ: max_consecutive_soft_failures=10, иначе
  снятие хайртриггера оставило бы прогон без тормоза вовсе.

Тесты: 10 проверок, включая дословный сценарий 5399 и страховку «генуинные
блоки по-прежнему рвут прогон после трёх». Проверено мутациями: возврат пустого
пула в блоки роняет 2, возврат транспорта — 6, снятие сторожа — 2. Четыре
существовавших теста на генуинный блок продолжают проходить без правки — это и
показывает, что разделение проведено по верному признаку. Набор целиком —
5206 passed, 37 skipped.
2026-08-30 16:33:56 +03:00
bot-backend
5f9dc5d512 feat(tradein/cian): у Циана не было добора карточек — только побочный эффект задачи про историю (#3284)
All checks were successful
CI Trade-In / 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) Has been skipped
CI / changes (pull_request) Successful in 12s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m2s
Карточки Циана доставались побочным эффектом cian_history_backfill, а её
выборка ключуется по offer_price_history. Следствия на 30.08: карточка есть
у 4494 из 25222 объявлений (17.8% — последнее место при втором месте по
объёму), 19046 без истории при квоте 100/сутки (190 дней на остаток, тогда
как очередь растёт вдвадцатеро быстрее), и 1697 объявлений с историей и без
карточки, которые исторической выборке недостижимы в принципе.

Фетчер при этом исправен: прогоны 5154/5240/5328 дали 100/100, 99/100,
100/100. Чинить нечего — не выдана мощность.

Добавлен второй режим выборки (listings_pending="detail", по
detail_enriched_at, свежие первыми) и второе расписание поверх ТОГО ЖЕ тела:
машинерия работает, дублировать её новым модулем незачем. Историческая
выборка оставлена побайтово — по ней живёт суточный прогон.

batch_size=400 не на глаз: замеренный темп ~28с на объявление, порог
reap_zombies 6ч по heartbeat, бюджетного сторожа у задачи нет — 400×28с≈3.1ч
проходит, 800 как у Яндекса (≈6.2ч) убивало бы жнецом.

Расписание засеяно enabled=false, как domclick_detail_backfill в миграции
175: это третий круглосуточный добор на общий пул из четырёх узлов, влияние
на соседей надо посмотреть, а не предположить.

Тесты: 13 проверок, ключ выборки / порядок / неизменность прежнего режима /
проводка параметров через посредника / регистрация обоих source. Проверено
мутациями: снятие ORDER BY, молчаливый дефолт вместо ValueError и потеря
listings_pending в посреднике роняют по 2-3 теста каждая. Набор целиком —
5196 passed, 37 skipped.
2026-08-30 15:34:36 +03:00
8fb5587888 Merge branch 'main' into fix/3194-hide-sql-parameters
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI / changes (pull_request) Successful in 19s
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) Successful in 3m10s
CI Trade-In / backend-tests (pull_request) Successful in 5m56s
CI / backend-tests (pull_request) Successful in 18m10s
2026-08-30 10:23:05 +00:00
663d63aea4 fix(tradein): подсказки адреса — пустые части адреса выпадают, а не печатаются
All checks were successful
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 / changes (pull_request) Successful in 16s
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 5m50s
Прод отдавал «г Екатеринбург, ул Фролова , д. 29, корп.»: реестровый
readable_address (ЕГРН) приходит УЖЕ склеенным вместе с пустыми частями —
маркер без значения печатается, лишний пробел перед запятой остаётся, и все
тиры /suggest (cadastral / geoportal / houses / DaData / Nominatim) пропускали
строку насквозь.

Чиним склейку, а не конкретный случай: `tidy_address()` — один проход по
частям (схлопнуть пробелы, выбросить пустые и маркер-без-номера:
корп./стр./лит./кв./оф./пом. и пр.), склейка обратно. Применяется в
`GeocodeSuggestion.__post_init__` — единственной точке, через которую проходят
все тиры, включая локальные, которые питают и `geocode()`.

Тест на функцию склейки, фальсифицирован снятием фильтра пустых: краснеет по
значению строки (10 из 14), а не исключением.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 14:58:03 +05:00
7e59c1e5b0 fix(#3194): hide_parameters=True на всех движках, include_local_variables=False у scheduler
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 / frontend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 7s
CI / openapi-codegen-check (pull_request) Successful in 3m10s
CI Trade-In / backend-tests (pull_request) Successful in 5m47s
CI / backend-tests (pull_request) Successful in 18m3s
Ключ шифрования кук и сами куки уезжали в GlitchTip: сервисы сессий передают
их bind-параметрами в pgp_sym_encrypt(:cookies_json, :key), а SQLAlchemy при
ошибке печатает ВСЕ параметры в тексте StatementError.

Правка на уровне движка (backend + tradein-mvp: db.py, auth_db.py,
alembic/env.py) кроет все сайты вызова разом, включая четвёртую копию в
scraper-kit и всё будущее.

scheduler_main.py был единственным из трёх sentry_sdk.init без
include_local_variables=False — процесс скрейпера, в кадрах лежат прокси-креды.

НЕ закрывает: текст ошибки самого драйвера (Postgres DETAIL со значением) и
сырые psycopg-подключения мимо движков — отдельный класс.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 14:53:41 +05:00
bot-backend
58e18e6fec fix(tradein/avito): браузерный путь ходил на каждую карточку холодным (#3251)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
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 / browser-tests (pull_request) Successful in 1m28s
CI Trade-In / backend-tests (pull_request) Successful in 4m58s
Пройденный QRATOR proof-of-work выбрасывался после каждой карточки: `reuse_context`
во всём репозитории передавал ЕДИНСТВЕННЫЙ вызов — domclick_detail_backfill.py:346.
Значит каждый /fetch Авито шёл через sidecar'овский browser.new_page(), то есть
новый изолированный context с пустой банкой кук. В логе прод-сайдкара это видно
прямо: «PoW-челлендж снят за ~1000мс» печатается на КАЖДОЙ успешной карточке —
челлендж решается заново каждый раз, а не один раз на прогон.

Эффект той же правки у Домклика измерен и записан в domclick_detail_backfill.py:331
— 26 последовательных фетчей сайдкара дали 100% блоков, те же карточки в тёплом
контексте 5/5 за ~2с.

Второй холод — сам заход: providers/avito/detail.py звал fetch(full_url) голым,
без origin и без Referer, тогда как Домклик (#3247) идёт fetch(card_url,
origin=SERP, referer=origin). Существующий для Авито прогрев (warm_up_session)
живёт только в curl-пути и с 22.08 в проде мёртв — AVITO_DETAIL_BACKFILL_USE_CURL
выставлен в false.

Что сделано:
- avito_detail_backfill: reuse_context=True + request_context_reset() РОВНО один
  раз за прогон и только на AvitoBlockedError. Не на AvitoSidecarUnavailableError
  (подтип AvitoRateLimitedError — отказ нашего тракта, не бан площадки) и не на
  AvitoListingGoneError. Зеркалит #3212: сброс на каждый блок сам себя
  поддерживает — пропуск живёт в context'е, сброс его выбрасывает, повторная
  проверка с того же IP снова блокируется, одна осечка даёт каскад.
- fetch_detail: необязательные origin/browser_referer (имя referer уже занято под
  Referer curl-пути, это разные фетчеры и разные поля). Дефолт None → payload и
  поведение city_sweep/pipeline/admin не меняются.
- _serp_origin_for: городская SERP из URL карточки, хост берётся из самого url.

Попутно — дефект якорной вкладки сайдкара, найденный при переносе. _ensure_anchor_page
отдавала True на ЛЮБУЮ живую вкладку, не сверяя её с запрошенным origin. А origin у
обоих caller'ов выводится ИЗ URL карточки и меняется вместе с городом (Авито —
сегмент пути, Домклик — поддомен). После первой же карточки другого города Referer
называл выдачу, которую этот контекст никогда не открывал: ни куки её, ни тайминга,
площадка видит заявленный переход без единого следа. Ровно то, что #3258 запретил
делать фолбэкам якорного поиска. Добавлен _anchor_origins: origin сменился — вкладка
переоткрывается. Чинит и Домклик тоже.

Заход через поиск Яндекса (BROWSER_ANCHOR_VIA_SEARCH) для Авито НЕ включается —
расширять этот список без отдельного замера запрещает комментарий у самой константы.
Хранилище авторизованных сессий Авито (#3179/#3180) этой правкой не заменяется.

База для сравнения снята ДО выката и записана в #3251: доля блоков от попыток
46.9% / 74.2% / 96.4% / 69.8% / 52.9% / 62.3% по суткам 25-30.08. Сравнивать после
деплоя по доле блоков и обогащению за сутки, а НЕ по статусу прогона: ratio-критерий
обрывает КАЖДЫЙ прогон, и статус banned про площадку ничего не говорит.

Тесты: backend 5150 passed / 37 skipped, сайдкар 211 passed (было 206 + 5 новых на
переезд якоря), ruff чист.

Refs #3251, #3180, #3118, #3212, #3247, #3258
2026-08-30 11:47:15 +03:00
bot-backend
44633b0df4 fix(tradein/domclick): свип ходил на QRATOR без кук и вис на PoW каждым запросом (#3264)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI 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 4m53s
serp.py не передавал куки сессии вообще — слова cookie в файле не было.
Свип приходил к QRATOR с чистым браузером и был обязан решать
proof-of-work с нуля на каждый запрос. Прод, прогон 5330 (30.08
03:49-03:55): 9 запросов к bff-search-web.domclick.ru, все 9 зависли на
челлендже (перезагрузка 1/2, 2/2, отказ), status=failed, 0 лотов. Прокси
при этом ротировался (узел 9 → 10) — узел тут ни при чём.

Добор с тем же сайдкаром и тем же пулом в ту же ночь взял 37 карточек из
37 без единого блока. Разница ровно в куках.

Чинить это стало возможно только сейчас: свип ходит не на
ekaterinburg.domclick.ru, а на отдельный хост bff-search-web.domclick.ru,
и до правки _cookie_domain (PR #3262) куки легли бы на
.bff-search-web.domclick.ru, не совпав с сессией площадки. Теперь оба
хоста схлопываются в общий .domclick.ru.

Снимок приходит параметром снаружи, а не читается внутри kit: kit не
импортирует app.* (strangler-инвариант #2133). Поэтому джоба
domclick_city_sweep переопределена продуктовым Handler'ом — build_registry
это прямо допускает («последнее слово за продуктом»), а БД читает только
app-сторона.

Отсутствие сессии не авария: load_session вернул None → свип идёт как
раньше, без инъекции, факт логируется один раз.

Цена решения — override повторяет вызов kit-джобы целиком и может тихо с
ней разойтись. Добавлен тест, который зовёт оба джоба одинаково и
сравнивает наборы kwargs, допуская расхождение ровно в cookies. Проверен
мутацией: с искусственно добавленным в kit-версию аргументом краснеет,
без него зелёный.

Тесты: 245 passed, 1 skipped (domclick + parity).
2026-08-30 09:40:21 +03:00
1d45ac0747 feat(mera/estimate): ручка фактов дома для предзаполнения формы + гейт «этаж не выше дома» (#3257)
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 4m2s
Deploy Trade-In / build-backend (push) Successful in 1m4s
Deploy Trade-In / deploy (push) Has been cancelled
2026-08-29 20:34:00 +00:00
5da226a271 fix(mera/estimate): перефит хедоники поверх area-бакетного ratio — крупное жильё занижалось на 21% (#3255)
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 4m5s
Deploy Trade-In / build-backend (push) Successful in 1m1s
Deploy Trade-In / deploy (push) Successful in 1m34s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 12s
2026-08-29 20:19:27 +00:00
0bdc7227a3 docs(mera/estimate): сегментные множители откалиброваны на артефакте измерения — предупреждение в конфиге (#3249)
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 19:55:41 +00:00
e3f3e8f3b1 Merge remote-tracking branch 'origin/fix/audit-dkp-price' into fix/audit-all 2026-08-30 00:14:23 +05:00