955 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| cd95222d90 |
test(auth): порог пробы по темпу тиков, диагностика в лог CI
All checks were successful
CI Trade-In / changes (pull_request) Successful in 14s
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 6m17s
Абсолютное `len(probe_latencies) >= 10` слабело ровно в целевом сценарии: elapsed под нагрузкой растёт, и заблокированный цикл добирает 10 тиков за 3-5с. Сверяем темп: замер пул 65.5-66.3 тик/с против 1.0 тик/с у bcrypt в `async def`, порог 8.0 — геометрическая середина, запас x8 в обе стороны. Тики/медиану/темп печатаем безусловно через capsys.disabled(): у зелёного теста pytest захваченный вывод не показывает, а запас на общем раннере виден только когда всё прошло. |
|||
| ecd6d5c91d |
test(auth): проверять вынос bcrypt механизмом, а не секундомером
All checks were successful
CI Trade-In / changes (pull_request) Successful in 15s
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 5m53s
`probe_latencies[-1] < 0.5` мерил на общем раннере не свойство кода, а свободен ли CPU у соседей: 03.09 деплой Trade-In встал на «худший сторонний запрос 544мс» на коммите, который часом раньше прошёл на незанятой машине (#3343). Тест из #2712 проверял правильную вещь неправильной метрикой. Вердикт теперь дискретный: сверка пароля обязана исполниться НЕ в потоке событийного цикла (двойник bcrypt пишет `threading.get_ident()`). Загрузка раннера этого не подделает. Второй половиной остаётся счётчик тиков пробы: при bcrypt в `async def` цикл стоит всю секунду флуда и проба не успевает почти никогда, при выносе в пул успевает ~100 — порог 10 лежит посередине с запасом ×10 в обе стороны. Абсолютные латентности сохранены в тексте падения как диагностика, но не гейтят. Фальсификация: возврат `verify_password` из пула прямо в `async def` роняет тест на новом утверждении («сверка пароля исполнилась в потоке событийного цикла», `assert 8387963968 not in {8387963968}`), 5 прогонов подряд под load average 24 — зелёные. Closes #3343 |
|||
| 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
|
|||
| 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
|
|||
| 64ec4a1aad |
Merge pull request 'fix(scraper-kit): чекпоинт avito_city_sweep доживает до финализатора — 0/67 прогонов писали done_buckets' (#3330) from fix/3319-citysweep-checkpoint 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
|
|||
|
|
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` — только счётчик, имена не рендерит. |
||
|
|
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 трёх подстрок на проде. |
||
| 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 |
|||
| 18e0f1d999 |
test(tradein): изолировать test_rbac.py от состояния реестра (#3316)
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 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
CI-красное на голове ветки: 4 теста в tests/test_rbac.py ждали YAML-роль (kopylov=pilot, user1=pilot), а в CI-базе реестр засеян миграцией 193 (kopylov=manager, user*=employee) — DB-first резолвер честно отдавал роль из БД. Локально те же тесты были зелёными ровно потому, что БД нет и работал YAML-fallback: результат файла зависел от ОКРУЖЕНИЯ, а такой тест не проверяет ничего. Чинится не подгонкой чисел в ассертах, а изоляцией: файл проверяет ИМЕННО legacy-путь roles.yaml (разбор файла, globs, guard и /me на trusted-header), и теперь заявляет это явно — autouse-фикстура `_legacy_yaml_only` глушит реестр (`_registry_role` → None). Ассерты на YAML-роли после этого законны в любом окружении. Приоритет реестра, эквивалентность scope employee↔pilot и конфигурация kopylov (DB manager + YAML pilot) покрыты отдельно — tests/test_role_single_source.py. Проверено обоими способами: полный `pytest tests` без сида и он же с плагином-имитацией засеянного реестра (подменяется тот же шов, что и в проде, `identity_store.identity_session`) — 5290 passed, 35 skipped в обоих. |
|||
| 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. |
|||
| 41f21c4969 |
docs(scraper-kit): обоснование #3319 — что именно теряло чекпоинт
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI / changes (pull_request) Successful in 13s
CI Trade-In / browser-tests (pull_request) 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 5m13s
Ревью опровергло формулировку «точку терял каждый финализатор»: все четыре писателя в runs.py мержат jsonb (`counters || :counters`), записанный ключ переживал mark_done/mark_banned/mark_failed. Правка закрывает выходы РАНЬШЕ первого end-of-anchor heartbeat (cancel/дрейн на первом якоре, ранний done #1950 на якоре №1) — комментарий и докстринг переписаны на это. Замер «0 из 67 за 60 дней» назван тем, чем он является: запись появилась 26.08.2026 (#3074) при такте avito 7 суток, выборка почти вся из эры без механизма. Плюс тест на прогон без ключей (эра до #3074) — метка дрейна не меняет вердикт «нечего подхватывать». |
|||
| 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 роль.
|
|||
| 0ed934ea09 |
fix(scraper-kit): чекпоинт avito_city_sweep доживает до финализатора (#3319)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 12s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 14s
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 5m30s
0 из 67 прогонов за 60 дней имели done_buckets в counters: точку писала одна строка внутри цикла якорей, а каждый выход (mark_done — включая ранний #1950 «SERP собран, detail заблокирован», — mark_banned, mark_failed) отдавал голый counters.to_dict(). Точка держалась только на jsonb-мерже в runs.py, то есть на свойстве чужого модуля, которого этот файл не проверяет. - payload любого выхода собирается одной функцией _ckpt() — done_buckets несут все 14 записей, а не одна; - якорь, умерший по таймауту, больше не считается пройденным (тот же инвариант, что у generic-except): SERP мог успеть, detail нет, и резюм пропускал такой якорь навсегда при штатно завершившемся прогоне; - SIGTERM-дрейн помечается counters.interrupted=1 и участвует в резюме. Статус остаётся 'done' — ни один читатель статуса не меняется; метка та же, что у rosreestr_dkp-дрейна. 'done' в _RESUME_STATUSES НЕ добавлен: чистый полный обход резюмить нечего. Дрейн перед IMV-фазой помечен отдельно (imv_phase_drained) — якоря там пройдены все, подхват собрал бы ноль. |
|||
| dab2b43efc |
imv: HTTP 429 больше не приговаривает дом навечно
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m50s
Алерты 01.09 (house_imv: RuntimeError('HTTP 429') / ('HTTP 400')) вскрыли
дыру в классификаторе: _raise_for_status_categorized разбирает 401/403 и 5xx,
а ВЕСЬ остальной 4xx проваливается в resp.raise_for_status() и приезжает в
house_imv_backfill голым RuntimeError. Бэкфилл типизирует только
IMV*-исключения — дом получает ТЕРМИНАЛЬНЫЙ imv_status='error' и выпадает из
повторных пакетов навсегда.
429 — канонический ВРЕМЕННЫЙ отказ (rate limit), и механика повтора для таких
существует (transient_error + retry-lane + лимит попыток #2674). Замер на
проде: 19 домов заперты в error с причиной «HTTP 429» — ретраебельный отказ
стал вечным приговором.
429 и 408 теперь IMVTransientError. 400 НАМЕРЕННО оставлен терминальным: тело
безликое {"code":400,"message":"Bad Request"}, оснований считать его
временным нет, а ретраебельный 400 значил бы вечно долбить дома с реально
кривыми параметрами. Тест держит границу С ОБЕИХ СТОРОН — и «429 transient»,
и «400 НЕ transient».
Фальсификация: снятие ветки 408/429 даёт 2 failed по значению
(IMVTransientError не поднят), не ImportError. 9 passed, ruff чисто.
Ремонт уже запертых строк — отдельным шагом после мержа: UPDATE 19 домов
error→transient_error (починка разбора не чинит строки сама).
|
|||
| 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
|
|||
|
|
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 попыток», границы бюджета ручки). |
||
| 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> |
|||
| 72cb410b05 |
fix(tradein/avito): бан-страница площадки уезжала в транспортные сбои и не банила узел (#3283) (#3304)
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 13s
Co-authored-by: lekss361 <lekss361@gendsgn.local> Co-committed-by: lekss361 <lekss361@gendsgn.local> |
|||
| 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
|
|||
| 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
|
|||
| 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
|
|||
| dbb8ca4ec1 |
Merge pull request 'fix(tradein/avito): browser-путь добора не смотрел на HTTP-статус — отказ площадки уходил в ошибку парсинга (#3297)' (#3300) from fix/3297-avito-browser-status-check into main
Some checks failed
Deploy Trade-In / build-frontend (push) Blocked by required conditions
Deploy Trade-In / build-browser (push) Blocked by required conditions
Deploy Trade-In / changes (push) Has been cancelled
Deploy Trade-In / test (push) Blocked by required conditions
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
|
|||
|
|
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 чист.
|
||
|
|
5c10aee4c2 |
fix(tradein/avito): browser-путь добора не смотрел на HTTP-статус — отказ площадки уходил в ошибку парсинга (#3297)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
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) Successful in 5m4s
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
Замер прода 31.08.2026 (24 попытки через 3 узла) поймал три отказа, которые
доезжали до parse_detail_html и падали ValueError("Cannot extract item_id"):
страница 8172 байта, статус 439, title «Доска объявлений от частных лиц и
компаний на Авито». Отказ ПЛОЩАДКИ записывался generic-ошибкой разбора, узел
не ротировался и не банился, брейкер по доле его не видел.
Две независимые дыры, обе в browser-ветке fetch_detail:
1. last_response_status не читался вовсе. Curl-ветка того же файла статус
проверяет (`if sc in (403, 439) or is_firewall`), browser-ветка смотрела
только на HTML. Прод ходит именно browser-путём.
2. Маркер витрины-заглушки протух: искали «объявления на сайте авито», а
фактический title — «доска объявлений от частных лиц и компаний на авито»,
подстрока в нём не встречается.
Правка:
- новые константы _AVITO_DETAIL_BROWSER_BLOCK_STATUSES = {403, 439} и
_AVITO_DETAIL_BROWSER_RATELIMIT_STATUS = 429, источник каждого статуса
назван комментарием;
- проверка стоит ПОСЛЕ _is_detail_not_found (404 остаётся
AvitoListingGoneError) и ДО parse_detail_html;
- статус None (сайдкар старой версии, goto без статуса) отказом НЕ считается —
поведение прежнее, фолбэк на html-эвристики;
- 429 разведён с блокирующими статусами и поднимает AvitoRateLimitedError.
Разница не косметическая: на AvitoBlockedError оркестратор один раз за прогон
зовёт request_context_reset (#3251) и выбрасывает пройденный QRATOR-PoW. При
rate-limit контекст цел, сбрасывать его — значит проходить проверку заново с
того же IP. Зеркалит curl-ветку, где 429 тоже не блок;
- старый title-маркер не удалён, а дополнен снятым вживую: площадка может
отдавать обе формы.
Тесты (9): каждый статус по отдельности, None-статус не ломает разбор и не
подавляет html-эвристики, 404 побеждает блокирующий статус (порядок проверок),
оба title-маркера опознаются, 429 не является AvitoBlockedError.
Фальсификация: без правки detail.py 5 из 9 новых тестов падают.
Прогон: 397 passed, 1 skipped (-k "avito or cadence or scheduler"), ruff чист.
Замер намеренно жёстче прода (без прогрева сессии и органического перехода из
выдачи), поэтому доля таких отказов в проде из него НЕ следует — её покажет
счётчик после правки.
|
||
| 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
|
|||
| f7d428c0c5 |
Merge pull request 'витрина: строка без улицы больше не открывает страницу и игру' (#3291) from fix/mera-showcase-order-street into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 22s
Deploy Trade-In / build-browser (push) Successful in 41s
Deploy Trade-In / build-frontend (push) Successful in 2m41s
Deploy Trade-In / test (push) Successful in 4m22s
Deploy Trade-In / build-backend (push) Successful in 1m7s
Deploy Trade-In / deploy (push) Successful in 1m53s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 12s
|
|||
| 884a6b9cfe |
бэктест: режим выборки вразброс — ORDER BY id DESC садится на несколько улиц
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 5m12s
ORDER BY id DESC берёт последние ВСТАВЛЕННЫЕ строки, а Росреестр грузится
пачками по домам: соседние id это один дом и одна улица. Замер на проде
31.08.2026 по Екатеринбургу, выборка 200:
последние 200 по id : 38 разных улиц, до 22 сделок с ОДНОЙ улицы
случайные 200 : 129 разных улиц, максимум 6 с одной
На этой выборке стоит витрина лэндинга: 20 строк с ЧЕТЫРЁХ улиц, 16 из них с
двух. Владелец заметил это как «почти все сделки из одного района».
ЧТО ЗАМЕР ПОКАЗАЛ, а что нет. Пять прогонов по 200 сделок:
recent MAPE 15.18% покрытие 88.75%
scattered mera MAPE 14.96% покрытие 83.13%
scattered alpha MAPE 15.97% покрытие 88.34%
scattered bravo MAPE 13.69% покрытие 83.85%
scattered charlie MAPE 14.36% покрытие 86.23%
Заголовочная точность УСТОЯЛА — опубликованные 14,5% лежат внутри разброса
представительной выборки. Кластеризация её не раздувала.
А покрытие коридором опубликовано как 88% — это верх диапазона: при пересборке
выборки величина гуляет 83-88 при медиане около 86. Публикуется лучший прогон
из пяти как единственный. Правка самих чисел лэндинга — отдельным заходом.
УМОЛЧАНИЕ НЕ ТРОНУТО. Докстринг _sample_sql обещает дефолтному пути побайтово
тот же SQL ради замороженного регресс-гейта; смена умолчания молча обнулила бы
сравнимость всей истории замеров. Режим выбирается флагом --spread.
Порядок по md5(id||seed), а не random(): нужен ВОСПРОИЗВОДИМЫЙ порядок, иначе
два прогона отличаются и из-за правки, и из-за состава выборки, и разделить
вклады нечем.
Тест держит обе стороны и проверен фальсификацией: сделать вразброс умолчанием
— падает identity-проверка (сравнение текстов её бы пропустило), заменить md5
на random() — падает проверка воспроизводимости.
|
|||
| 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> |
|||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
| 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
|
|||
| 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> |
|||
| 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> |
|||
|
|
657fdbd1ba |
Merge remote-tracking branch 'forgejo/main' into fix/3267-block-marker-in-listing-text
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 4m51s
|
||
|
|
1bf9c4b632 |
test(tradein): гонка на sleep(0.05) вешала тест потолка оценок на 120с (#3270)
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 / 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 4m58s
Тест занимал все слоты семафора четырьмя висящими запросами и ждал, что они дошли до acquire, обычным сном на 50 мс. На нагруженном раннере этого не хватало: пятый запрос заставал свободный слот, входил в подменённую оценку и вставал на gate.wait() — а gate.set() стоит ниже по тому же корутину. Дедлок до pytest-timeout, две минуты простоя job'а и красный CI на постороннем PR (#3268). Сон заменён счётным барьером: подменённая оценка отпускает семафор при входе, тест дожидается ровно _CONCURRENCY входов. Вход означает, что слот уже захвачен, — это то самое условие, которое сон угадывал по времени. Проверено пробой с намеренно свободным слотом (держателей на одного меньше): старая структура висит до таймаута, новая падает за 12с с понятным сообщением. |
||
| 169121cea8 |
Merge pull request 'fix(tradein/avito): браузерный путь ходил на каждую карточку холодным (#3251)' (#3267) from fix/3180-avito-warm-context into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 14s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Successful in 2m19s
Deploy Trade-In / test (push) Successful in 4m11s
Deploy Trade-In / build-backend (push) Successful in 2m9s
Deploy Trade-In / deploy (push) Successful in 1m50s
Deploy Trade-In / deploy-status (push) Successful in 2s
Deploy Trade-In / perimeter-smoke (push) Successful in 13s
|
|||
|
|
aec05280fa |
fix(tradein/domclick): «система защиты от протечек» в объявлении читалась как блок QRATOR
Some checks failed
CI Trade-In / changes (pull_request) Successful in 10s
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) Failing after 7m5s
_extract_json искал маркеры блока подстрокой по ВСЕМУ телу ответа — до
разбора JSON, то есть и по пользовательским описаниям объявлений.
Прод 30.08, прогон 5363: продавец написал в описании квартиры «🔹 Система
защиты от протечек». Подстрока совпала с маркером «система защиты», ответ
на 107 183 байта с двадцатью валидными офферами был объявлен блок-
страницей, свип оборвал все комнатные корзины и забанил живой узел пула
на 6 часов. Совпасть так же могут «captcha» и «qrator» — в тексте
объявления, в имени агентства, в ссылке.
Порядок перевёрнут: сначала разбор JSON, маркеры — только если разбор не
удался. Разобранный JSON нужной формы блок-страницей быть не может,
QRATOR отдаёт HTML, так что валидный разбор сам по себе доказывает
отсутствие блока. Тот же порядок давно применён в detail.py — там
маркеры смотрят только когда __SSR_STATE__ не найден; свип был
единственным местом с обратной логикой.
Мусор без маркеров теперь ValueError, а не блок: за неразобранный ответ
неизвестной природы узел банить нельзя.
Тест проверен мутацией: со старым порядком 8 проверок из 11 краснеют,
включая дословный фрагмент описания из прогона 5363.
|
||
|
|
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 |
||
|
|
5baf07d6e6 |
fix(tradein/domclick): свип брал BFF навигацией браузера — теперь подзапросом (#3264)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 / browser-tests (pull_request) Successful in 1m28s
CI Trade-In / backend-tests (pull_request) Successful in 4m53s
BFF-ручка Домклика не страница, а JSON-эндпоинт SPA. Сайдкар умел ровно одно — page.goto(url), — и навигацией браузера на API-хост мы делали то, чего настоящий клиент не делает никогда. Перехват сети на живой выдаче 30.08 это и показал: офферы приезжают в SSR-документе, а к BFF ходят XHR'ы за гео, районами и метро. Через мобильные узлы пула такая навигация упиралась в ChallengeTimeout: прогоны 5330 и 5351 — 9 из 9 и 6 из 6 запросов зависли на челлендже, 0 лотов. Карточки через те же узлы в те же минуты шли. Замер трёх режимов на одном узле, один и тот же ресурс: BFF navigate ChallengeTimeout, 55с BFF subresource HTTP 200, 52 байта, 6с BFF page_fetch ошибка Подзапрос проверен вширь: все шесть комнатных корзин HTTP 200, суммарно 6359 офферов против 6367, снятых напрямую с резидентного IP (расхождение — дрейф фонда за пару часов); список офферов отдаётся на всех четырёх узлах пула, по 20 штук, 120 КБ. Сайдкар получил fetch_mode: navigate (дефолт, прежнее поведение), subresource (context.request.get из прогретого контекста) и page_fetch (fetch из страницы). Третий оставлен, потому что теоретически он ближе всего к настоящему XHR, но в замере отказал — выбор сделан измерением, а не рассуждением. Прогрев origin обязателен: рукопожатие QRATOR попадает в куки контекста именно при заходе на страницу, поэтому свип теперь передаёт origin и referer, которых раньше не передавал вовсе. _decode_body распаковывает gzip по магическим байтам — карта офферов приезжает Content-Type: application/gzip, и без этого вызывающий получил бы бинарь в поле "html". Сама карта, к слову, подзапросом всё равно не берётся (279 байт — загрузчик QRATOR), но BFF полнее: 100% фонда против 62% у карты. fetch_mode кладётся в payload только когда он не дефолтный — сайдкар прежней версии не должен получать незнакомый ключ (тот же приём, что с referer в #3247). Тесты: сайдкар 212 passed (новый test_server_fetch_mode.py — 6 проверок), backend 316 passed (новый test_3264_sweep_subresource_mode.py — 5). Три тестовых дублёра _do_fetch/_post_fetch знали старую сигнатуру — обновлены. |
||
|
|
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). |
||
| 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
|
|||
| 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
|
|||
| e523c8949c |
fix(mera/backtest): свежая фикстура не реплеилась, а разрез по сегментам врал по построению (#3254)
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 4m2s
Deploy Trade-In / build-backend (push) Successful in 1m7s
Deploy Trade-In / deploy (push) Successful in 1m58s
Deploy Trade-In / deploy-status (push) Successful in 2s
Deploy Trade-In / perimeter-smoke (push) Successful in 12s
|
|||
|
|
ff0a15d443 |
feat(tradein/browser): ходить по Домклику как человек — с Referer и с перезагрузкой зависшего рукопожатия
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 / 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 5m1s
Добор карточек Домклика упирался в отказ на 11-й карточке: прогон 5298 дал attempted=13, enriched=10, blocked=3. Ручные прогоны в живом браузере брали 91 и 40 карточек без единого отказа. Разбор нашёл два отличия, и оба оказались нашими, а не площадки. 1. Referer не отправлялся НИКОГДА. playwright'овский goto() по умолчанию этот заголовок не шлёт, а параметр `referer`, который он принимает, мы не передавали. Площадка видела десяток появлений подряд прямо на URL карточки, без источника перехода — так не ходит ни один человек. Комментарии в коде при этом уверяли про «органическую навигацию с реальным Referer»; они врали, теперь исправлены. Сайдкар принимает `referer` в теле /fetch и ставит его ТОЛЬКО на целевую навигацию; на origin и якорную вкладку не ставит — туда приходят «сами». Добор Домклика передаёт страницу выдачи, чем переход и является по смыслу. 2. Зависшее рукопожатие не перезагружалось. _wait_out_pow_challenge построен на допущении «страница перезагрузит себя сама после решения PoW»; ручная сессия 29.08 через узел 10 это опровергла — выдача осталась на 401, и пропуск qrator_jsid2 выдался только после ДВУХ перезагрузок, сделанных руками: 102.7с GET → 401, 115.1с GET → 401, 118.4с GET → 200, следом кука-пропуск, и карточка за 6 секунд. Пока мы только опрашивали content(), такая страница жила до таймаута, а бэкфилл засчитывал это в блоки. Теперь после BROWSER_CHALLENGE_RELOAD_AFTER_MS (8с) сайдкар перезагружает сам, не больше BROWSER_CHALLENGE_MAX_RELOADS (2) раз за фетч. Оба пути безопасны на откат: без поля `referer` в теле поведение прежнее, BROWSER_CHALLENGE_RELOAD_AFTER_MS=0 возвращает прежний опрос без навигаций, упавшая перезагрузка не роняет фетч — опрос продолжается в том же бюджете. Тесты: 191 passed в сайдкаре (было 182) — 4 на Referer, 5 на перезагрузку, в том числе «страница ожила сама → лишней навигации нет» и «висит вечно → не больше лимита». Точечные backend-тесты домклика и scraper_kit — 144 passed. |
||
| e3f3e8f3b1 | Merge remote-tracking branch 'origin/fix/audit-dkp-price' into fix/audit-all | |||
| df9dd52996 |
fix(mera/public): Infinity/NaN во входе — 422, и бюджет считает такие запросы
Аудит живого сайта 30.08.2026: POST /api/public/mera/coverage с
{"lat":56.8,"lon":1e400,...} отвечал 500, и двенадцать таких запросов подряд
дали двенадцать пятисоток и ни одного 429. Две независимые поломки в одном
месте, обе воспроизведены локально до правки.
1. 500 вместо 422. json.loads принимает Infinity/-Infinity/NaN, а 1e400 даёт
inf переполнением. Pydantic отбивает такое поле по границам и кладёт
значение в input ошибки, а ответ об ошибке сериализуется
json.dumps(allow_nan=False) и падает уже после входа в ответ. Ломается не
поле, а сборка ответа об ошибке — одна на всё приложение, поэтому и
обработчик один (app/core/http_errors.py), а не валидатор на lon.
2. Лимитер мимо. _enforce стоял первой строкой тела хендлера, а FastAPI
валидирует тело позже зависимостей, но раньше тела — до проверки просто не
доходило. Та же поправка места, что уже сделана сегодня у
_require_public_estimate_enabled: перенос в dependencies. Сделано для всех
ручек файла, не только coverage. У /estimate и /estimate/read флаг остаётся
первой зависимостью — 429 на выключенной ручке подтверждал бы её
существование.
Тесты двусторонние: снятие обработчика роняет 4 проверки 422, возврат лимитера
в тело роняет проверку бюджета (проверено).
|
|||
| c467584de3 |
fix(mera/landing): «Цена ДКП» — цена договора, а не произведение; экспозиция считает и Домклик
Витрина показывала fact_rub = price_per_m2 * area_m2, хотя deals.price_rub лежит в той же строке и не использовалась. price_per_m2 в базе integer, поэтому под подписью «Цена ДКП» ехала реконструкция: 4 799 995 вместо 4 800 000, 3 649 995 вместо 3 650 000 (прод, сделки 5777343 и др.). Теперь price_rub едет из выборки (DealSample.price_rub) и показывается как есть; err_pct считается от той же величины. Строка без price_rub НЕ показывается — подставлять реконструкцию в одну строку из двадцати значило бы спрятать тот же дефект (на проде price_rub заполнен у 33 555 из 33 555 сделок выборки витрины). Вторая находка аудита (listing_date якобы «когда увидели МЫ», экспозиция занижена втрое) НЕ ПОДТВЕРДИЛАСЬ. listing_date пишут cian (added_ts), yandex (creationDate) и avito (дата карточки выдачи) — это дата публикации у источника. Там, где заполнены и listing_date, и publish_date, они совпадают: yandex 10 761 из 10 903, avito 474 из 569, медиана разницы 0 дней. 75 дней у аудитора — эффект другой ВЫБОРКИ: publish_date есть у 15 058 активных строк (yandex + Домклик, оба старые), listing_date — у 25 982 (плюс cian с медианой 17 дней и 87% avito с медианой 19). Настоящий дефект рядом: по одному listing_date Домклик выпадал целиком (0 из 3061 активной строки), метрика считалась по 83.6% активных объявлений, и подпись об этом молчала. COALESCE(listing_date, publish_date) → охват 95.2% (29 568 из 31 068), медиана та же — 26 дней; охват теперь назван в note. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| 2390eec740 |
fix(mera/b2c): вернуть координаты сделки, снесённые моим же коммитом про студию
Коммит |
|||
| ca3f073b0e |
feat(mera/b2c): улица сделки доезжает до витрины готовой схемой, а не геометрией
Карта в карточке игры показывала полигон района — единственную геометрию, до которой у tradein был доступ. Улицы живут в базе gendesign, foreign table и гранта на них не было. Мост по образцу соседей (v_tradein_cad_buildings / v_tradein_osm_poi_ekb): gendesign 195 — вьюха v_tradein_osm_roads_ekb (highway + water) с GRANT В ТОМ ЖЕ ФАЙЛЕ (после #3227 грант отдельной миграцией теряется при пересоздании); tradein 281 — foreign table gendesign_osm_roads_ekb плюс колонки street_name / street_scheme в landing_showcase_deals. Пересчёт витрины кладёт в строку УЖЕ СПРОЕЦИРОВАННЫЕ SVG-пути окна 840x840 м вокруг центра улицы. Проекция — та же равнопромежуточная с cos(широты), что в export_ekb_districts_svg.py; второй в проекте нет. Замер 2026-08-29: GeoJSON того же окна 7-8 КБ на строку, схема — 2.8-2.9 КБ. ЧТО ДАННЫЕ ВЫДЕРЖИВАЮТ, И НИ СЛОВОМ БОЛЬШЕ · Это УЛИЦА, а не дом: deals.address уровня улицы, номер дома у 2.7% сделок. В схеме намеренно НЕТ координат окна и констант проекции — точку дома по ней нельзя поставить даже случайно. Это замок, а не забывчивость. · Зданий нет: cad_buildings — 18 307 контуров на город, в плотном центре 51 здание на радиус 450 м, где их в разы больше. Нарисованная застройка заявляла бы полноту, которой в данных нет. · Улицы — фильтрованная выгрузка «источников шума»: именованные покрыты хорошо, дворовые и служебные проезды отсутствуют. · Улица сматчилась у 550 названий из 654 — 31 410 сделок из 34 021 (92.3%). Остальным street_scheme = NULL, и это штатно: фронт показывает район, который для этого и оставлен. · Перекрёсток («Челюскинцев/Шейнкмана») берём первой улицей: таких адресов три на 34 021 сделку, и обе улицы одинаково верны на уровне улицы. · Наличие схемы НА ОТБОР СТРОК НЕ ВЛИЯЕТ — то же правило, что запрещает отбор по величине ошибки: иначе витрина показывала бы не работу оценщика, а те 92% адресов, что удобно легли на OSM. Тесты (каждый сломан вручную и покраснел): нормализация на реальных адресах включая ё/е и «8 Марта»; недоступная вьюха и упавший запрос дают None, а не исключение; схема не раздувается — потолок в байтах на реальной плотности плюс прямая проверка округления до 0.1. |