698 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 673c02e5d6 |
Merge pull request 'fix(tradein): писатели наконец пишут то, что обещает схема — фото подсказок, статус «снято», события объявлений (#2674)' (#2682) from fix/2674-writers-honor-schema into main
All checks were successful
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 2m50s
Deploy Trade-In / build-backend (push) Successful in 1m32s
Deploy Trade-In / deploy (push) Successful in 6m36s
|
|||
| 3e1b9a8b0d |
fix(tradein): чинит такт загрузки СберИндекса — иначе новый ERROR стал бы ложной тревогой (#2674)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / 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 2m59s
CI / changes (pull_request) Successful in 7s
Ревью PR #2681 опровергло исходную посылку по СберИндексу, и это подтвердилось на моих же числах (все 24 прогона монитора, read-only): 13-16.07 alert=1 age 73..76 latest=май 17.07 alert=0 age 46 latest=июнь ← день загрузки 18-31.07 alert=0 age 47..60 01-05.08 alert=1 age 61..65 Загрузка ходила раз в 28 дней и приносила период на месяц новее, возраст считается от первого числа покрытого месяца → пол 46, потолок 74, порог 60 ВНУТРИ диапазона. Тревога срабатывала 14 суток из 28 без всякого застоя источника: девять срабатываний были замером нашего собственного такта. Поднятие до ERROR без этой правки завело бы ежедневное ложное событие две недели в месяц. Миграция 212 переводит sber_index_pull на недельный такт (потолок ≈53 при пороге 60, запас 7 суток) вместо поднятия порога до 75 (запас 1 сутки — ломается от любого сдвига окна). Цена: 9 запросов в неделю вместо 9 в 28 дней к публичному sberindex.ru/api/sowa; прогон 4 секунды, 0 ошибок за всю историю. Дополнительно по ревью: - поллер Росреестра: ветка «файл найден в листинге, но HEAD не отдал zip» → ERROR (ровно поведение старой Bitrix-заглушки) + вписана в таблицу уровней; - тестовый харнесс закрывает клиент событий (фоновый поток на каждый тест). Refs #2674 |
|||
| 43aaf91b97 |
fix(tradein): писатели наконец пишут то, что обещает схема — фото подсказок, статус «снято», события объявлений (#2674)
All checks were successful
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 7s
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 2m56s
Три находки одного класса из эпика: колонка есть, писатель есть, тест на писателя зелёный — а данные не появляются. Тестами это не ловится по построению, только сверкой с продом. 1. house_suggestions: парсер выбрасывал imageLink, а INSERT не перечислял image_link + area_m2/rooms/floor/total_floors. 25 055 строк с NULL во всех пяти колонках, ~74 дня с миграции 064. Метрики парсятся из title тем же _parse_title, что и у placementHistory. 2. listings_snapshots.status: 'active' у всех 394 299 строк при 55 448 реально неактивных объявлений. Оба места вызова с литералом 'active' честны — там объявление действительно видели; не писал никто ветку «снято». Теперь оба места деактивации пишут снимок 'closed' в ТОЙ ЖЕ транзакции: TTL-задача (data-modifying CTE, все 4 источника через один deactivate_stale_listings) и 404 из avito_detail_backfill. Дата снятия перестаёт быть догадкой. 3. listing_source_events: схема знает 5 типов, писался 1 (price_change, 8288 строк). Дописаны ветки delisted/relisted/edited/first_seen в тот же set-based statement — данные для них уже лежат в снимке. JOIN → LEFT JOIN LATERAL, иначе first_seen недостижим по построению; план #2607 (per-row index point-lookup по idx_lss_source_date) сохранён, проверено EXPLAIN на проде. Счётчики прогона теперь по типам, все пять всегда присутствуют — ровно они показали бы четыре нуля из пяти. Миграция не нужна: все колонки и CHECK уже существуют. Тесты: tests/test_2674_writers_honor_schema.py. Гейты сверяют писателя со СХЕМОЙ (колонки INSERT против CREATE TABLE 064, типы событий против CHECK 079), поэтому ловят и следующую забытую колонку. Фальсификация патч-методом: без фикса 1 — 6 красных, без фикса 2 — 6, без фикса 3 — 4. |
|||
| 46bbb79881 |
fix(tradein): сигналы о сбоях наконец становятся событиями, а протухание кук предупреждает заранее (#2674)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
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 2m58s
CI / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
В скрапер-контейнере GlitchTip поднят с LoggingIntegration(event_level=ERROR), поэтому любой сигнал уровня WARNING событием не становится — сколько бы раз он ни срабатывал. Прод это подтвердил: монитор устаревания СберИндекса отработал 24 раза, 9 из них со staleness-вердиктом, событий ноль; куки Домклика протухли 2026-08-03 и об этом никто не узнал. Разбирали не «поменять warning на error», а по каждому сигналу: сбой, из-за которого данные перестают обновляться — событие; рутина и ожидаемые состояния — лог. Плюс предупреждение ЗАРАНЕЕ там, где чинить нужно руками (куки Домклика — по образцу #2658 для Циана, переиспользован тот же подход session_expires_at + COOKIE_EXPIRY_WARN_DAYS). У поллера Росреестра выход нового квартала оставлен уровнем info, но получил явный capture_message(level="info"): новость хорошая, но требует ручного импорта оператором, а INFO-строка живёт только до ближайшего редеплоя. logger.error для неё был бы враньём в error-rate и стрик-алертах. Оговорка: у GlitchTip-проекта сейчас нет ни правил, ни получателей (#2673) — события станут видны в интерфейсе, но никому не отправятся. Refs #2674 |
|||
| 4b4ab8b34c |
fix(tradein/imv): счётчики прогона в total_seen/new_count + лог дрейфа ремонта (#2674)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 2m51s
По ревью PR #2675. 1. counters прогона не заполняли выделенные колонки. _column_counts (scrape_runs.py) берёт total_seen из ключей total_seen|lots_fetched и new_count из new_count|lots_inserted — ни одного из них в дикте не было, поэтому все 39 прогонов этого source лежат в БД с total_seen=0. А mark_done по этой же колонке шлёт алерт «3 подряд done с нулевым результатом» (#2625): даже идеальный прогон с 50 сохранёнными считался бы нулевым и через три дня выстрелил бы ложной тревогой про капчу. Добавлены total_seen=checked и new_count=saved. Трейд-офф назван в комментарии: на исчерпанной очереди checked=0 три дня подряд тоже даст алерт — но пустая очередь при ежедневном расписании это и правда сигнал. 2. _map_renovation_type молча схлопывал в 'cosmetic' любое незнакомое непустое значение. Сегодня в проде ровно четыре канонических, живого эффекта нет, но дрейф вокабуляра реален (70950 строк listings с пустым нормализованным ремонтом). Добавлен logger.debug на случай «непустое, но не в карте» — паритет с house_type_normalizer, который такой лог уже пишет. 3. Обоснование дефолта 'cosmetic' в докстринге заменено на более сильное по данным: это одновременно МОДА и МЕДИАННАЯ категория популяции (standard 7984 / good 7116 / needs_repair 4738 / excellent 2562; кумулятивно needs_repair 21.2%, +standard 56.8%), то есть наилучшая одиночная догадка, а не просто «не край шкалы». Там же названа асимметрия: поштучный путь эстиматора при неизвестном ремонте IMV вообще не зовёт, а домовой дефолтит — решение осознанное (иначе теряем ещё ~32% домов очереди), чтобы следующий читатель не принял это за недосмотр. Refs #2674 |
|||
| 0815319e1c |
fix(tradein/imv): домовая оценка перестаёт врать про ремонт и тип дома (#2674)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 7s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 2m51s
Три дефекта в house_imv_backfill, найденные системным поиском (эпик #2674/#2673).
1. Тип ремонта был захардкожен литералом 'cosmetic' — все 2685 запросов ушли
как «косметический ремонт», хотя мода repair_state по объявлениям тех же
домов другая: standard 4564 / good 4118 / needs_repair 2279 / excellent 1631
(косметика лишь 36%). Теперь renovation_type берётся из mode(repair_state)
в том же агрегате, что уже считает медианы комнат/площади/этажа, и проходит
через существующий estimator._IMV_REPAIR_MAP (ленивый импорт — estimator
тянет scraper_adapters, а тот импортирует этот модуль). Второго словаря не
заводим. Неизвестный ремонт (498 домов из 2685) остаётся 'cosmetic': это
середина порядковой шкалы required < cosmetic < euro < designer, а не край,
системного сдвига в одну сторону не даёт.
2. Неизвестный тип дома молча становился 'panel' — и когда типа нет вовсе, и
когда он есть, но не совпал со словарём. Панель почти самый дешёвый класс
(медиана по нашим же 2685 оценкам: block 122.6k < panel 128.8k <
brick 131.1k < monolithic 145.9k руб/м2), то есть дефолт систематически
занижал. На проде так уехали 363 дома совсем без типа и 75 домов с
camelCase-типом из Циана (56 из них monolithBrick — минус 11.7% против
monolithic). Теперь сырое значение прогоняется через общий
scraper_kit.house_type_normalizer.normalize_house_type (знает monolithBrick /
gasSilicateBlock / aerocreteBlock / stalin и SCREAMING-вокабуляр Яндекса),
дефолт 'panel' убран: тип не распознан → house_type=None → дом помечается
no_params ('unknown house_type') и запрос к площадке не тратится. 'other' и
'wireframe' намеренно НЕ маппятся — честного соответствия у них нет.
3. Прогон не умел падать: 31 прогон подряд с saved=0 и ~35 ошибками из 50
помечен 'done'. Тот же класс, что #2670/#2657 — успех определялся как «не
поймали известное исключение». Теперь saved=0 при errors>0 → mark_failed.
Ноль сохранённых БЕЗ ошибок (всё отфильтровано в skipped) остаётся done.
Балкон/лоджия оставлены константами намеренно: покрытие listings.has_balcony
13.8%, listings.balcony_loggia 9.4%, и колонки противоречат друг другу (по
has_balcony «есть» у 62%, а по balcony_loggia самый частый случай — loggia
5650 против balcony 2794). Мода по одному-двум объявлениям на таком покрытии —
шум, а не данные.
Причина, по которой бэкфилл не сохранил НИ ОДНОЙ оценки за 34 дня, — вне этого
модуля и здесь не чинится (детали и числа в описании PR): 1240 домов легли на
отказе браузерного сайдкара «нет прокси» (гейт #2616, 05.07-02.08), а после
возврата прокси 05.08 — 23 на Page.evaluate «Execution context was destroyed»
в tradein-browser и 12 на 403 Авито.
Refs #2674
|
|||
| 96d62e418b |
Merge pull request 'fix(tradein/scraper): пропуск расписания пишет строку прогона со статусом skipped (#2658)' (#2662) from fix/2658-loud-skip-status into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 9s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 2m6s
Deploy Trade-In / test (push) Successful in 2m42s
Deploy Trade-In / build-backend (push) Successful in 1m33s
Deploy Trade-In / deploy (push) Successful in 2m11s
|
|||
| d173163025 |
fix(tradein): тест ловит копию константы, а не equality; честный комментарий про вклад свежести (#2660)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / 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 2m44s
По ревью PR #2664. 1. test_freshness_window_is_the_estimator_constant_not_a_copy проверял `lc.LISTINGS_FRESH_DAYS is estimator.LISTINGS_FRESH_DAYS` — CPython кэширует малые int, поэтому скопированный литерал `LISTINGS_FRESH_DAYS = 14` тест бы ПРОШЁЛ, хотя докстринг обещает ловить ровно это. Прошлая фальсификация срабатывала лишь потому, что откат удалял имя целиком (AttributeError). Теперь проверяем исходник через inspect.getsource — фальсифицировано подстановкой копии литерала вместо импорта: тест краснеет. 2. Комментарий в location_index.py приписывал свежести чужую заслугу. Прод-разложение: из −14.8% сдвига городской медианы −14.7 п.п. даёт сегментный гард и лишь −0.18 п.п. свежесть. Для этой метрики свежесть — не коррекция смещения, а страховка на будущее, оплаченная третью пула (3 504 вторичных строки, из них 2 724 живые) и ростом дисперсии: на центре ЕКБ n 423 → 86, индекс гуляет по выбору окна на 12-14 п.п. Размен верный, но он должен быть написан как размен. Там же задокументирован новый режим отказа: свежесть связала витрину со здоровьем сбора — встанет скрейпинг на 14 дней, и insufficient_data прилетит всем пользователям разом. Учитывая, что #2574 это месяц молчаливой поломки сбора, сценарий не гипотетический. Окно свежести не меняю — вопрос вынесен отдельно. Refs #2660 |
|||
| b800760c24 |
fix(tradein/scraper): фильтр skipped в админке + освежение схлопнутой строки (#2658)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 7s
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m3s
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 2m46s
Правки по ревью PR #2662. Фильтр статуса. `GET /admin/scrape/runs?status=skipped` отдавал 422 — 'skipped' не было в Literal, а во фронте не было чипа. Строки рисовались, но задать вопрос «что сейчас пропускается» на единственной поверхности, построенной ровно для этого, было нельзя. Добавлено в оба места (translateStatus «пропущено» и нейтральный бейдж уже умели). Схлопывание освежает строку. UPDATE двигал только finished_at/heartbeat_at, из-за чего живой стрик замерзал: списки прогонов сортируют ORDER BY started_at DESC и берут limit=20, поэтому 37-дневный пропуск утонул бы под свежими прогонами других источников — след в базе есть, на экране нет. Теперь started_at = NOW(), а начало стрика переезжает в counters.first_skip_at; сортировку общего списка не трогаем (она про все источники, чинить надо было одну строку). Там же обновляется counters.detail — иначе в строке 37 дней висел текст «протухли 1 день назад», хотя именно эта цифра и есть предмет issue. jsonb_set заменён на `||` + jsonb_build_object: три вложенных jsonb_set читать в 3 ночи невозможно, а NULL в jsonb_set обнуляет весь counters. Поиск последней строки. `ORDER BY id DESC` не ложится на индекс (source, started_at DESC) из миграции 015 — для unknown_source (тикает каждые 60 с бессрочно) это отбор всех строк источника с сортировкой раз в минуту. Теперь ORDER BY started_at DESC, id DESC. session_expires_at получил valid_only: предупреждение «скоро протухнут» считает срок ИМЕННО той записи, которую взял load_session — при нескольких аккаунтах свежайшая-любая может быть чужой протухшей строкой. Диагностика после None по-прежнему смотрит на свежайшую любую (валидных там нет по определению). Запись пропуска намеренно НЕ обёрнута в свой try/except: если db.execute падает, то падает и claim следующего расписания в этом же тике — тик срывается в любом случае, а глушить исключение здесь значило бы вернуть ровно тот немой пропуск, ради которого заведён #2658. Самовосстановление через 60 с. |
|||
| 837ad8cfd4 |
fix(tradein): сегментный гард в «медианном торге», свежесть в индексе локации, честные админ-счётчики (#2660)
All checks were successful
CI / changes (pull_request) Successful in 7s
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 2m42s
CI Trade-In / changes (pull_request) Successful in 6s
CI Trade-In / frontend-checks (pull_request) Has been skipped
Пользовательская половина разбора #2574: витрины читают listings без сегмента и без свежести, поэтому показывают числа, посчитанные не по тому пулу. 1. Миграция 211 — гард #1186 в window_listings у street_sales_vs_listings(). 27.3% кандидатов на пару «ДКП ↔ объявление» были новостройками, и девелоперский прайс (который не торгуется) формировал показываемый процент торга. is_active здесь по-прежнему НЕ фильтруется — осознанно: функция намеренно смотрит и снятые объявления, иначе к сделке нечего подставить. Сигнатура не меняется, значит CREATE OR REPLACE — замена, а не вторая перегрузка (грабли #2627 закрыты тестом-сравнением сигнатур с м.205). 2. location_index — предикат свежести + сегментный гард в обоих запросах медианы, симметрично _COMMON_WHERE эстиматора. Витрина обязана смотреть на тот же пул, на котором считается цена; окно свежести берётся импортом LISTINGS_FRESH_DAYS, второго определения константы не заводим. 3. /scraper/data-quality и /cache-stats — «активно» не прячем, а разделяем: рядом отдаётся «из них не виделись N дней» (+ сам порог N в ответе). Именно слепой count(*) WHERE is_active заставлял #2574 месяц выглядеть как «всё собирается». Refs #2660 |
|||
| 0b54b96984 |
fix(tradein/scraper): пропуск расписания пишет строку прогона со статусом skipped (#2658)
All checks were successful
CI / changes (pull_request) Successful in 7s
CI Trade-In / changes (pull_request) Successful in 8s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 2m41s
Пропуск наступившего окна был немым: logger + сдвиг next_run_at, ни строки в scrape_runs, ни изменения last_run_at. cian_history_backfill так простоял 37 дней на протухших куках Циана и снаружи выглядел работающим — next_run_at исправно двигался вперёд, а docker-логи с warning'ом терялись на каждом редеплое. Статус 'skipped' заведён ещё миграцией 015 и локализован во фронте («пропущено»), но в проде имел 0 строк — механизм построен и ни разу не использован. Задействуем его во всех пяти местах, где расписание пропускалось без следа: kit `_claim_run` (already_running / concurrent_claim / running_appeared_under_lock), kit `scheduler_loop` (unknown_source) и продуктовый cian `pre_claim`. Причина — слаг в `error`, по нему «нет кук» отличается от «уже бежит» запросом, а не грепом логов. Подряд идущие одинаковые пропуски схлопываются в одну строку со счётчиком `counters.skips`: «уже бежит» и «неизвестный source» не двигают next_run_at и иначе плодили бы строку каждый тик (60 с). Алерт про куки жил в недостижимой ветке: он стоял там, где verify_session вернул None, а на протухших куках load_session сам фильтрует expires_at_estimate > NOW() и отдаёт None ещё в первой, немой ветке. Теперь алерт в обеих ветках и через logger.error — в scraper-контейнере GlitchTip поднят с LoggingIntegration (event_level=ERROR), поэтому прежний capture_message(level="warning") событием не становился. Плюс предупреждение ЗАРАНЕЕ (COOKIE_EXPIRY_WARN_DAYS=5) в том же pre_claim: обновление кук — ручная операция, алерт по факту протухания приходит, когда сбор уже встал. Монитор нулевых прогонов (#2625) не трогаем: обе alert-выборки отбирают failed/banned/done/cancelled, поэтому 'skipped' в стрик не попадает и его не прерывает — пропуск не «прогон вернул ноль лотов», смешивать нельзя. |
|||
| 828e32e9ab |
fix(tradein/estimate): вернуть канарейку бэктеста и карточку IMV после сброса anchor_tier (#2656)
All checks were successful
CI / changes (pull_request) Successful in 7s
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / 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 2m43s
По итогам глубокого ревью PR #2661. 1. Ослабление replay-стаба выключало канарейку «реплей разошёлся с захватом» навсегда и для всех будущих PR, а не только для 19 сделок этой правки. Теперь число неотвеченных lookup-вызовов считается и выносится в метрику unrecorded_lookup_calls — baseline сравнивает целые ТОЧНО, поэтому 19 стало закоммиченной константой: новый незаписанный путь снова валит гейт громко, а перезахват фикстуры доведёт число до нуля. Перезахват фикстуры (лучший путь по ревью) не сделан осознанно: он требует живой прод-БД tradein и тянет НОВУЮ выборку сделок, то есть не «дозаписывает» 19 ответов, а меняет саму систему отсчёта регресс-гейта по причинам, не связанным с этим PR. Попутно найдено и починено: документированная регенерация baseline (--from-fixture --update-baseline) с #2173 писала baseline, который гейт не мог совпасть НИКОГДА — тест пиннит estimate_dedup_analogs_enabled=False, а CLI нет. Пин переехал внутрь replay_fixture, теперь оба пути согласованы. Регенерированный baseline отличается ровно одной строкой (новый ключ), ни одна метрика не сдвинулась. 2. Сброс anchor_tier открывал щель в отображении: комплы якоря добыты, якорь не построен, headline подавлен → не срабатывал ни blend, ни display-only блок, и пользователь терял карточку Avito IMV. Гейт по tier снят, защита от двойного заполнения (`avito_imv_summary is None`) остаётся. 3. Тест мест теперь стережёт и inline-копию _COMMON_WHERE в Tier W — самое вероятное место следующего расхождения того же сорта. Фальсификация: правка baseline 19→18 валит гейт; снятие предиката из Tier W валит тест мест; возврат старого условия display-блока валит новый тест карточки. |
|||
| 2878a88c67 |
fix(tradein/estimate): фильтр свежести в якоре дома и знаменателе коэффициента выкупа (#2656)
All checks were successful
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 2m45s
Цена местами строилась на объявлениях, которых никто не видел месяц. Главный радиусный путь эстиматора нёс `scraped_at > NOW() - LISTINGS_FRESH_DAYS дней` (_COMMON_WHERE), а четыре денежные выборки — нет: - `_fetch_anchor_comps` Tier A и Tier C: якорь ЗАМЕЩАЕТ headline (median_ppm2/median_price/n_analogs), т.е. правит цену напрямую; - `asking_to_sold_ratio` ask_side и ask_global: знаменатель коэффициента, на который умножается expected_sold_price. `is_active` свежесть не заменяет — он означает разное у разных источников (TTL деактивации 30 дней, NULL-сегмент не деактивируется никогда), а `scraped_at` одно и то же. Прод: 21 132 из 37 497 активных строк протухли по 14-дневной мерке самого эстиматора и были полностью годны для якоря. Окно вынесено в app.core.config.LISTINGS_FRESH_DAYS (одно место на всех): держать его в estimator.py нельзя — тот сам импортирует area_bucket из app.tasks.asking_to_sold_ratio, обратный импорт дал бы цикл. Второй половиной — залипший anchor_tier: он оставался "C"/"A", когда якорь не был построен, и молча глушил IMV-blend, quarter-index (#764 Guard-1a), radius-floor и corridor-clamp-exempt Tier A. Теперь сбрасывается явно, у источника, для всех трёх причин (None из _compute_same_building_anchor, гейт Tier C #1795, low-conf гейт #audit-1). Обе половины одним PR намеренно: порознь они дадут два заметных скачка цены вместо одного меньшего (якорная половина −3.63%, знаменатель +1.25%, вместе −2.42% от суммы выкупа на 1040 реальных оценках). Тесты: tests/test_freshness_filter_2656.py — предикат во всех четырёх местах, единственность константы, бинд :fresh_days, протухший комп не в пуле якоря, сброс anchor_tier и разглушённый IMV-blend. Все 7 краснеют без фикса (проверено git stash). |
|||
| 17fcf746f7 |
Merge remote-tracking branch 'origin/main' into fix/2603-geocode-city-hint-tails
All checks were successful
CI / changes (pull_request) Successful in 7s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 8s
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 2m41s
# Conflicts: # tradein-mvp/backend/app/api/v1/admin.py |
|||
| 5ecd5361fd |
fix(tradein/geocode): гейт мусорного города вынести в общий хелпер и прошить в admin-путь (#2603)
All checks were successful
CI / changes (pull_request) Successful in 7s
CI Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 2m42s
Первый коммит починил только scripts/geocode_deals_nominatim.py — ручной скрипт. Тот же дефект оставался на живом пути: POST /admin/geocode-missing?target=deals отдавал сырой row["city"] в city_hint, а deals.city росреестровое и в хвосте распределения содержит не-города («Бессонова», «Билейский рыбопитомник»). Любой не-ЕКБ хинт жёстко закрывает EKB-локальные тиры и уезжает префиксом в запрос провайдеру, то есть мусорный хинт хуже отсутствия хинта. Гейт вынесен в geocoder.known_city_hint (сверка с SVERDLOVSK_OBLAST_CITIES — тем же набором, который уже питает _names_non_ekb_city / _ekb_local_tiers_allowed) и переиспользуется всеми тремя потребителями city_hint: скриптом, admin-ручкой и задачей geocode_missing. Копий функции нет — четвёртый потребитель, если появится, получит гейт сам. Тесты: мусорный город -> хинт не передаётся, валидный -> передаётся; проверено фальсификацией (без фикса все три новых теста краснеют). |
|||
| 00bc07a55f |
fix(tradein/proxy): backup-узел должен быть пригоден + рычаг снятия бана (#2600 п.2)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 7s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 2m45s
Правки по deep-review PR #2654. MEDIUM. Внутренний EXISTS считал backup'ом любой enabled-узел affinity. До п.2 это было эквивалентно «пригоден», потому что бан выключал узел глобально; теперь узел бывает enabled и одновременно забанен СВОИМ же источником. Fallback мог увести последний реально рабочий узел выделенной affinity (два domclick-узла, один забанен domclick'ом → второй уходит под avito → domclick без прокси). Добавлено требование, что backup не забанен своим источником — в acquire и зеркально в защите mark_banned. MEDIUM. У оператора не осталось способа снять бан: в п.1 ложное срабатывание лечилось PATCH enabled=true (он обнулял disabled_reason), теперь бан живёт в отдельной таблице и истекает только по таймеру, до 72ч при эскалации. Добавлен proxy_pool.clear_source_bans; зовётся из patch_proxy при ручном включении и после УСПЕШНОЙ ротации exit-IP (бан привязан к proxy_id, а банился IP — после смены адреса строка держала бы узел вне выдачи без причины). LOW. Тест защиты дублировал логику вместо её проверки: ban-предикаты в фейксессии теперь гейтятся по подстрокам боевого SQL (как в acquire-ветке) — проверено мутацией, тесты краснеют при удалении NOT EXISTS из запроса. LOW. Конверсия в миграции 210 матчила disabled_reason по LIKE 'banned:%' и могла отменить ручное выключение оператора (формат подсказан комментарием 209-й) — сужено до точного списка значений домена provider_affinity. LOW. Docstring report_ban в browser_fetcher описывал старую модель (enabled=false); формула в COMMENT ON COLUMN была на шаг мимо (срок ТЕКУЩЕГО бана, не следующего). Расхождение с acquire по leased_by зафиксировано в докстринге как осознанное. Refs #2600 |
|||
| 964867a943 |
feat(tradein/proxy): здоровье прокси по паре «узел × источник» (#2600 п.2)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / 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 2m43s
Бан площадкой был глобальным: п.1 на распознанный бан выключал узел целиком (enabled=false, disabled_reason='banned:<source>'). Реальность другая — Авито банит IP, а Яндекс через тот же IP ходит чисто, поэтому один забаненный источник выкидывал живой узел из пула для всех и худил пул быстрее, чем его пополняют (#2638). Плюс такое состояние не самолечилось: ipify площадку не эмулирует, бан не видит, а non-NULL disabled_reason блокирует авто-воскрешение (#2610) — нужен был ручной PATCH. Теперь бан — свойство ПАРЫ (proxy_id, source) в scrape_proxy_source_bans: acquire(source) не выдаёт узел только этому источнику, для остальных узел первосортный; снимается сам по времени. Срок эскалирует 6ч → 12 → 24 → 48 → 72 (потолок) на повторных банах той же пары; ban_count сбрасывается purge'ем истёкших строк через 7 суток — поэтому purge намеренно отложенный, а не по banned_until < now(). Защита последнего узла сохранена, но считается по источнику: если после бана у acquire(source) не останется кандидатов — бан не пишется, WARNING зовёт пополнять пул. Миграция 210 конвертирует прод-остатки п.1 (enabled=false + disabled_reason LIKE 'banned:%') в 6-часовые per-source баны и возвращает узлы в строй — иначе они висели бы выключенными вечно. Оператору активные баны видны в GET/PATCH /admin/proxies (source_bans) — без этого «узел включён, но не выдаётся» необъяснимо. Refs #2600 |
|||
| d362b16d7c |
fix(tradein/proxy): доводить сигнал бана площадки до пула (#2600 п.1) (#2653)
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 2m39s
Deploy Trade-In / build-backend (push) Successful in 1m35s
Deploy Trade-In / deploy (push) Successful in 1m40s
|
|||
| aa5bb76822 |
fix(tradein/proxy): отличать ручное выключение узла от авто-выключения (#2610) (#2652)
All checks were successful
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 2m35s
Deploy Trade-In / build-backend (push) Successful in 58s
Deploy Trade-In / deploy (push) Successful in 1m17s
|
|||
| 59c072fc4c |
chore(tradein): удалить мёртвые mobileproxy env-переменные и rotate-ip (#2616 шаги 2-3) (#2650)
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Successful in 2m13s
Deploy Trade-In / test (push) Successful in 2m45s
Deploy Trade-In / build-browser (push) Successful in 3m2s
Deploy Trade-In / build-backend (push) Successful in 1m38s
Deploy Trade-In / deploy (push) Successful in 2m29s
|
|||
| 90332e9827 |
fix(tradein): area-бакеты в asking→sold — и расчёт, и применение (#2620) (#2648)
All checks were successful
Deploy Trade-In / changes (push) Successful in 9s
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 2m36s
Deploy Trade-In / build-backend (push) Successful in 1m0s
Deploy Trade-In / deploy (push) Successful in 1m28s
|
|||
| 2aa79e2143 |
fix(tradein): честный радиус поиска аналогов на карте v2 (#2632) (#2643)
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 2m0s
Deploy Trade-In / test (push) Successful in 2m39s
Deploy Trade-In / build-backend (push) Successful in 1m51s
Deploy Trade-In / deploy (push) Successful in 1m23s
|
|||
| 86ea09f2d5 |
fix(tradein/scraper): капча Cian и заглушки Yandex — banned, не done (#2625) (#2642)
All checks were successful
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 2m34s
Deploy Trade-In / build-backend (push) Successful in 1m35s
Deploy Trade-In / deploy (push) Successful in 1m57s
|
|||
| ef2b07ee9c |
fix(tradein/proxy): один прокси на сессию браузера вместо смены на каждом запросе (#2640)
All checks were successful
Deploy Trade-In / changes (push) Successful in 9s
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 2m36s
Deploy Trade-In / build-backend (push) Successful in 1m33s
Deploy Trade-In / deploy (push) Successful in 1m26s
Co-authored-by: lekss361 <lekss361@gendsgn.local> Co-committed-by: lekss361 <lekss361@gendsgn.local> |
|||
| 7d13e93792 |
fix(tradein/scraper): не ходить через мёртвый запасной прокси (#2634)
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Successful in 38s
Deploy Trade-In / build-browser (push) Successful in 2m18s
Deploy Trade-In / test (push) Successful in 2m44s
Deploy Trade-In / build-backend (push) Successful in 1m37s
Deploy Trade-In / deploy (push) Successful in 2m27s
Co-authored-by: lekss361 <lekss361@gendsgn.local> Co-committed-by: lekss361 <lekss361@gendsgn.local> |
|||
| 8b79e9659b |
fix(tradein/estimate): не строить цену по одному-двум аналогам, не отдавать 0 ₽ (#2629)
All checks were successful
Deploy Trade-In / changes (push) Successful in 9s
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 2m36s
Deploy Trade-In / build-backend (push) Successful in 1m4s
Deploy Trade-In / deploy (push) Successful in 1m15s
|
|||
| 0abfe020df |
fix(tradein/geocoder): строгий матч литеры дома + починка DaData region-фильтра (#2622)
All checks were successful
Deploy Trade-In / changes (push) Successful in 9s
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 2m36s
Deploy Trade-In / build-backend (push) Successful in 59s
Deploy Trade-In / deploy (push) Successful in 1m26s
|
|||
|
|
86afbf9e6f |
fix(tradein/estimate): не применять хедонику к неправдоподобному году постройки
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 7s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 2m40s
house_metadata (OSM/кадастр) отдаёт year_built без валидации; на проде встретился 1829, который клампился в нижний пол хедонического фактора (estimate_hedonic_factor_min=0.75) и резал выкупную цену на фикс. -25% без физического смысла — модель никогда не видела осмысленного объёма домов старше 1955 (см. COHORTS). Год вне [1917, текущий+3] теперь трактуется как отсутствующий (None, нейтральный year-term) вместо клампа, с WARNING-логом по house_id/адресу. Единая точка входа _sanitize_build_year() в estimate_quality() перед cohort-фильтром, house-match scoring и хедоническим фактором — покрывает оба источника года (payload.year_built и house_meta.year_built). |
||
|
|
b586b5ff68 |
fix(tradein/snapshot): починить зависающий запрос снапшотов и добавить бюджет времени (#2607)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Successful in 2m37s
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
Root cause: event-diff CTE джойнил "today" (снимок за CURRENT_DATE) с "prior" (DISTINCT ON по всей listing_source_snapshots, ~2.6-2.8M строк) обычным JOIN. Планировщик оценивал today в 1 строку (свежевставленные в той же транзакции строки ANALYZE ещё не видел) → Nested Loop без Materialize пересчитывал DISTINCT ON по всей таблице заново на каждую из ~80-140k реальных строк today (EXPLAIN на проде: cost≈300k на этом шаге) — прогон не укладывался ни в 6h zombie-порог, ни в сутки, каждую ночь минимум с 19 июля. Переписано на JOIN LATERAL (per-row indexed point-lookup через idx_lss_source_date, cost упал до ~4.4/строку). Плюс budget_sec → SET LOCAL statement_timeout как defense-in-depth (по образцу geocode_missing_listings) — задача теперь честно падает в mark_failed вместо того чтобы висеть сутками, если план когда-нибудь разрегрессирует снова. Зомби-детектор (reap_zombies) не тронут — он только помечает scrape_runs.status, не убивает backend (нет pid/application_name в схеме run'а); pg_terminate_backend для этого — отдельный follow-up, не в этом PR. |
||
| 887df54908 |
Merge pull request 'feat(tradein/proxy): ротация exit-IP ASocks по бану со счётчиком и громким отказом (#2600)' (#2611) from feat/tradein-asocks-rotation 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 2m39s
Deploy Trade-In / build-backend (push) Successful in 1m3s
Deploy Trade-In / deploy (push) Successful in 1m2s
|
|||
|
|
bed2b7bca9 |
fix(tradein/proxy): pin ASocks rotate_url host — не слать токен на чужой прокси (#2600)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / 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 / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 2m46s
scrape_proxies.rotate_url колонка неоднородна: прод несёт и mobileproxy changeip-ссылки (id 3/4/5), и ASocks-ссылки (id 1/9/10/11). Без явной проверки хоста Authorization: Bearer <ASOCKS_API_TOKEN> ушёл бы на чужой провайдер — security review PR #2611. Добавлен ALLOWED_ROTATE_HOST-пиннинг (https-only, хост == api.asocks.com) ДО HTTP-вызова; несовпадение — отказ, не безголовый запрос без Authorization (смысл ручной ротации — конкретный провайдер). Заодно: класс исключения (не секрет) в note сетевой ошибки — отличить ConnectError от ReadTimeout; расширено leak-покрытие на текст log/Sentry сообщений (не только reason/note). |
||
|
|
b1563b86cb |
feat(tradein/proxy): ротация exit-IP ASocks по бану со счётчиком и громким отказом (#2600)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / 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 2m40s
|
||
|
|
876b666424 |
fix(tradein/proxy): не отдавать в fallback последний узел выделенной affinity (#2600)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / 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 / 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 2m44s
Ревью PR #2609: domclick — ровно один узел (прод scrape_proxies.id=1), намеренно вырезанный из общего пула через provider_affinity='domclick' (см. 173_scrape_proxies_add_domclick_affinity.sql) — QRATOR банит всё, кроме этого одного чистого residential-адреса. Fallback-запрос из предыдущего коммита мог законно забрать его под avito/cian/yandex, оставив domclick (сейчас исправно собирает: 6501 активных объявлений, 368/сутки) без прокси вообще — чинили бы один источник ценой полной поломки другого. - acquire(): fallback-SELECT дополнен условием "affinity='any' ИЛИ есть ДРУГОЙ enabled-узел той же affinity" через коррелированный EXISTS- подзапрос (WHERE + FOR UPDATE SKIP LOCKED + ORDER BY last_ok_at NULLS LAST, id — сохранены). Кандидат с единственным enabled-узлом своей выделенной affinity в fallback не участвует. - Тесты: единственный domclick-узел → acquire('avito') возвращает None; второй enabled domclick-узел появляется — fallback снова срабатывает. - Починен мок FakeSession (tests/services/test_proxy_pool.py): ветка "mark_health ok" раньше ставила enabled=True безусловно по совпадению общей подстроки "SET consecutive_fails = 0" (одинаковой в старом и новом SQL) — test_mark_health_ok_revives_disabled_proxy проходил бы и против кода без реанимации. Теперь ставит enabled=True только если в тексте SQL реально есть "enabled". Та же проблема была и в fallback-ветке (protects_last_node переопределял логику в Python независимо от SQL) — исправлено аналогично: применяется, только если в SQL реально есть EXISTS-подзапрос. |
||
|
|
ad753c6a87 |
fix(tradein/proxy): самовосстановление пула и запасной прокси чужой affinity (#2600)
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 / backend-tests (pull_request) Successful in 2m42s
Прод-замер: disabled-узлы никогда не перепроверялись (WHERE enabled в run_proxy_healthcheck) — auto-disable по DISABLE_THRESHOLD необратим, транзиентный сбой = вечный приговор (id 11 сгорел за ночь, будучи физически исправным). acquire() при пустой выборке по provider_affinity падал в None, морив источник голодом при живых свободных узлах чужой affinity. - run_proxy_healthcheck: disabled-узлы проверяются реже (DISABLED_RECHECK_MINUTES=60 либо last_check_at IS NULL); успешная проба реанимирует узел (enabled=true через mark_health) и инкрементит новый счётчик revived. - mark_health(ok=True) теперь безусловно ставит enabled=true (реанимация). - acquire: вторым заходом при пустой выборке своей affinity берёт любой свободный здоровый узел любой affinity (WARNING-лог), приоритет своих сохранён. - _probe_proxy классифицирует неуспех (timeout/connect_error/http_error/other) в fail_kind — прокидывается в mark_health только для логирования; полноценное разделение порогов транзиент/бан отложено (см. docstring mark_health). |
||
| eccb895db1 |
feat(tradein): переключаемый реестр людей — подготовка переезда «Меры» в БД auth [PR-2b/6]
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 7s
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 1m3s
CI Trade-In / backend-tests (pull_request) Successful in 2m40s
Дефолт не меняет ничего: IDENTITY_STORE="tradein" — это сегодняшний прод, tradein_users/tradein_sessions, соединение с БД auth не открывается вообще. Переключение делается одной переменной окружения ПОСЛЕ того, как на проде появится пароль auth_app и будут скопированы данные. Так сделано намеренно: мерж, который зависит от невыполненного ручного шага, — это мерж, который ломает прод в момент невнимательности. Ядро. app/services/identity_store.py — единственное место, знающее, в какой БД и в каких таблицах живёт реестр. Имена таблиц берутся из фиксированного словаря по значению флага, не конкатенацией с вводом. app/core/auth_db.py — ЛЕНИВЫЙ engine БД auth (core/db.py создаёт свой на импорте; такое же для auth роняло бы старт без DSN). Одно понятие состояния доступа вместо двух. В tradein_users состояние — булев is_active, в auth.users — access_state из трёх значений. Конверсия живёт в одной функции to_access_state(): True→active, False→disabled, а неизвестная строка, NULL или чужой тип → disabled с WARNING. Fail-closed выбран сознательно: если следующая миграция добавит четвёртое состояние, оно по умолчанию НЕ будет пускать. Проверка доступа — свойство can_sign_in, а не сравнение со строкой. Логин в режиме auth. Пароль проверяется ВСЕГДА и ДО ветвления по состоянию — иначе появляется timing-oracle и перечисление логинов. Верный пароль + trial_expired → 403 с машиночитаемым code="access_expired", сессия НЕ создаётся. Верный пароль + disabled → тот же generic 401, что и при неверном пароле. Резолв уже выданной сессии пропускает только active — блокировка обрывает сессию немедленно, а не по истечении sliding-refresh. Старт падает явно, если IDENTITY_STORE=auth, а DSN не задан. Без этого ошибка конфигурации не похожа на аварию: продуктовая БД жива, приложение работает, а rbac_guard ловит исключение резолва вместе с любым другим сбоем и падает в legacy trusted-header ветку — то есть сутками раздаёт права из roles.yaml мимо реестра, включая аккаунты с disabled. Форма входа понимает новый код ответа. Ветвление по detail.code, а не по тексту: текст бэк вправе менять, код — нет. Гранты соблюдены, а не обойдены: auth_app не имеет UPDATE на role/manager_id и не имеет DELETE на users (миграция 004, column-level). Тесты: 2996 passed (+59). Единственный красный — test_search_cache_hit — предсуществующий: проверен контрольным полным прогоном на чистом main (2937 passed, тот же красный). |
|||
|
|
3d075632a9 |
chore(tradein/geocoder): удалить Яндекс-геокодер (#2593)
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 / backend-tests (pull_request) Successful in 2m28s
Убирает ядро Yandex Geocoder (forward/reverse/suggest lookups + region-check + bias-хелперы + EKB_BBOX dict) из app/services/geocoder.py — Yandex demo-key исчерпан, Nominatim/DaData/локальные ЕКБ-тиры (geoportal/cadastral) остаются единственными живыми провайдерами. Цепочка тиров после удаления: кэш → геопортал ЕКБ → кадастр (house-match) → кадастр (raw) → Nominatim; в подсказках дополнительно DaData. НЕ затронуто (намеренно): Yandex.Недвижимость как источник объявлений (source='yandex', yandex_city_sweep*, providers/yandex/serp.py:geocoderAddress), Avito geocoder (providers/avito/imv.py:_geocode), EKB_BBOX_TIGHT/WIDE, _nominatim_region_ok, scripts/*_yandex_reverse.py и их тесты, tests/fixtures/ yandex_geocode_sample.json (всё ещё используется test_audit_address_mismatch.py). _SNAP_PRECISIONS оставлен с "exact" (недостижимо без Yandex-tier, но дёшево хранить — parity с frontend MapPicker.tsx SNAP_PRECISIONS и не ломает test_snap_precision_useful_exact_and_number). |
||
| fdf222cc5c |
Merge pull request 'fix(tradein/geocoder): fail-closed выбор ЕКБ-реестров вместо списка городов (#2582)' (#2589) from fix/tradein-geocoder-fail-closed into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 2m18s
Deploy Trade-In / build-backend (push) Successful in 1m4s
Deploy Trade-In / deploy (push) Successful in 1m13s
|
|||
|
|
af609c18f3 |
fix(tradein/geocoder): починить маркер села + fail-closed для явного city_hint (#2582)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / 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 2m20s
H1 (deep-review PR #2589): `с\.` в _LOCALITY_MARKER_RE был мертворождён — хвостовой \b не может сработать между "." и пробелом (оба не-словесные), формат «с. <село>» (104/77к прод-адресов) не ловился НИ РАЗУ, зато ложно срабатывал на инициалах «им с.п.королева». Живой инцидент воспроизведён: "с. Новоалексеевское, ул. 8 Марта,37" снапалось в ЕКБ (совпадение до 5-го знака, село в 35 км западнее). Заменил на lookahead-форму для однобуквенных маркеров (с/д/дер/пгт/ст): "\.\s*(?=[а-яё])" — различает "д. 5" (дом-номер, не матчит) от "д. Кашино" (деревня, матчит), что заодно позволило безопасно включить "д."/"дер." (раньше исключённые из-за коллизии с house-маркером). M2: гейт по city_hint оставался list-based (_names_non_ekb_city(city_hint)) — для СТРУКТУРНОГО хинта (не свободного текста) fail-closed форма должна быть обратной: "не Екатеринбург → закрыто", а не "есть в списке 37 → закрыто". city_hint принимает произвольную строку без валидации (API напрямую, минуя 6-городный UI-selector) — city_hint="Реж" держал гейт открытым, geoportal отдавал точный ЕКБ-дом, city_ambiguous=False делало это уверенно-неверным. M3: добавлены изолирующие тесты — предыдущая параметризация "незнакомый топоним" всегда несла ещё и "обл."/"республика"/"край" одновременно с проверяемым сигналом, поэтому мёртвый "с\." прошёл незамеченным. Плюс честно зафиксирована известная дыра (гейт по city_hint) — "реж, ленина 5" без маркера остаётся в ЕКБ-дефолте (архитектурное решение отдельно, не список городов). docstring `_names_unrecognized_locality`/`_ekb_local_tiers_allowed` приведён в соответствие с фактическим поведением regex. |
||
|
|
174680a82e |
Merge forgejo/main into fix/tradein-geocoder-fail-closed
All checks were successful
CI / changes (pull_request) Successful in 12s
CI Trade-In / 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 2m35s
Разрешён конфликт в geocoder.py между #2580 (city_hint threading в локальный гейт + dual-query Tier 4 Nominatim) и #2582 (fail-closed структурный маркер топонима вместо списка городов): `_ekb_local_tiers_allowed` теперь принимает `city_hint` и закрывает гейт по OR трёх независимых сигналов — известный не-ЕКБ город в тексте (#11), known-city в city_hint (#2580 C1), незнакомый топоним-маркер в тексте (#2582). Добавлены тесты на все 4 достижимые ветки (hint=известный город / маркер в тексте / ничего / hint=Екатеринбург); полный набор тестов (свои + пришедшие из #2580) зелёный одновременно. |
||
| 790f69ee01 |
Merge pull request 'fix(tradein/rbac): скрыть «Доля в продаже» и «Кэш» от клиентских аккаунтов' (#2587) from fix/tradein-hide-saleshare-cache-from-clients into main
Some checks failed
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 13s
Deploy / build-frontend (push) Has been skipped
Deploy Trade-In / changes (push) Successful in 24s
Deploy / build-backend (push) Successful in 52s
Deploy / build-worker (push) Successful in 52s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy / deploy (push) Successful in 2m4s
Deploy Trade-In / test (push) Successful in 3m7s
Deploy Trade-In / build-backend (push) Has been cancelled
Deploy Trade-In / build-frontend (push) Has been cancelled
|
|||
|
|
61b329e724 |
fix(tradein/geocoder): fail-closed выбор ЕКБ-реестров вместо списка городов (#2582)
EKB-only локальные тиры (geoportal/cad_buildings) держались на списке из 37 городов области: "нет в списке -> считаем Екатеринбургом". Молча ломало любой адрес вне списка, включая ДРУГИЕ РЕГИОНЫ РФ целиком -- подтверждено живыми случаями в geocode_cache (Ялта/Крым, Трёхгорный/Челябинская обл., пос. Лёвиха, Димитровград/Ульяновская обл. -- все получили координаты ЕКБ). Новый гейт `_ekb_local_tiers_allowed`: локальные тиры разрешены только если Екатеринбург подтверждён явно, ЛИБО в адресе вообще не назван город/регион (bare "Ленина 1" -- основной трафик формы оценки, дефолт ЕКБ оправдан и сохранён без изменений). Если назван ЛЮБОЙ другой топоним -- известный город списка (`_names_non_ekb_city`, без изменений) или незнакомый (`_names_unrecognized_locality` -- новый структурный маркер "обл./край/ республика/г./пос./с.", БЕЗ "д." -- коллизия с house-маркером "д. 5") -- гейт закрыт, идём во внешние oblast/nation-aware провайдеры. city_hint-логика #2580 не тронута (тот же файл, сосед по цели). |
||
| 8d3b571925 |
Merge pull request 'fix(tradein/geocoder): не подставлять Екатеринбург молча — явный city_hint (#2576)' (#2580) from fix/tradein-geocoder-city-default into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 27s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / test (push) Successful in 2m32s
Deploy Trade-In / build-backend (push) Successful in 1m17s
Deploy Trade-In / deploy (push) Successful in 1m27s
|
|||
|
|
abb9398f3f |
fix(tradein/rbac): скрыть «Доля в продаже» и «Кэш» от клиентских аккаунтов
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI / changes (pull_request) Successful in 11s
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 2m19s
CI Trade-In / backend-tests (pull_request) Successful in 3m13s
CI / openapi-codegen-check (pull_request) Successful in 3m34s
CI / backend-tests (pull_request) Successful in 16m7s
Аккаунт praktika (DB-роль manager) видел оба пункта в топбаре на /trade-in/team.
Это внутренние инструменты — аналитика рынка и состояние кэшей/скраперов, —
клиентские аккаунты их видеть не должны (решение владельца продукта).
Гейт один — deny-список роли, потому что все три места сверяются с ним через
общий матчер: пункт меню (Topbar по scopePath из /me), страница (RouteGuard) и
серверные ручки (rbac_guard). Правка только фронта спрятала бы пункт, оставив
прямой URL и API открытыми.
Закрыто для employee/manager (DB_ROLE_PATHS) и для legacy pilot (roles.yaml):
/trade-in/sale-share/**
/trade-in/cache/**
/trade-in/api/v1/buildings/**
/trade-in/api/v1/trade-in/cache-stats/**
У cache-stats ГЛОБ, а не точный путь: точный паттерн — строгое равенство, его
обходит трейлинг-слэш ('…/cache-stats/' → allowed=True), и защита держалась бы
на Starlette redirect_slashes, а не на RBAC. Замерено после правки: все варианты
(слэш, %2f, ./, ../) дают 403, утечек нет.
Основной продукт не задет: buildings.py обслуживает ТОЛЬКО sale-share, секция
«Продажи в доме» на экране оценки питается estimate-хендлерами. admin и analyst
сознательно вне deny — запиннено тестом, иначе «синхронизация» списков закрыла
бы их молча.
Заодно починен КРАСНЫЙ pre-existing тест главного бэкенда:
backend/tests/test_rbac.py::test_get_role_known_users ждал pilot у всех
user1..user10, но user2 («Брусника») стал expired 2026-07-30. CI это пропустил —
auth/roles.yaml не входит в paths-filter backend/**, из-за чего сьют не бежал.
Тесты: 153 passed (tradein) + 24 passed (site-finder, было 23+1 failed).
Новые — e2e через реальный rbac_guard по session-ветке (именно ею ходит
praktika), пин deny_paths в выдаче /me, границы глоба и regression-guard'ы.
Проверены снятием deny: 7 тестов краснеют, т.е. не тавтологии.
|
||
|
|
b3c76c8c62 |
fix(tradein/geocoder): city_hint должен доходить до локальных тиров + не терять ЕКБ-привязку в Tier 4 (#2576)
All checks were successful
CI / changes (pull_request) Successful in 9s
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 Trade-In / backend-tests (pull_request) Successful in 2m19s
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Deep-review PR #2580 нашёл два блокера в предыдущем фиксе (#2576): C1 — city_hint не участвовал в гейте локальных ЕКБ-only тиров (geoportal/cadastral, `use_local_ekb` в geocode() и Tier 1 в suggest()). Явный city_hint="Нижний Тагил" для "Ленина 1" всё равно попадал на ЕКБ-only базы, получал "точный" ЕКБ-хит и при этом city_ambiguous=False (хинт ведь был) — то есть система теперь ложно-уверенно утверждала неверный город. Фикс: city_hint участвует в той же проверке _names_non_ekb_city (гейт/ gazetteer #2582 не трогаю — только добавляю вход). C2 — снятие суффикса ", Екатеринбург" в _nominatim_suggest для случая "город неизвестен" регрессило часть реальных ЕКБ-адресов: без текстовой подсказки о городе Nominatim иногда предпочитает street-level матч в соседнем городе-спутнике (эмпирика ревьюера: "Победы 20" без суффикса → Верхняя Пышма вместо ЖК "Парк Победы" в Екатеринбурге). Решение — dual-query: bare (честный, без города) И ЕКБ-suffixed запросы объединяются (не заменяют друг друга), оба честных кандидата остаются в подсказках, пользователь выбирает сам. Extra round-trip только для последнего fallback-тира (cadastral/DaData/Yandex уже не сработали) — не задевает основной трафик. Заодно (🟠, дешёвая правка): city_hint прокинут в _nominatim_lookup — с тех пор как Yandex-ключ недействителен (#2585), это единственный живой внешний провайдер, и его tie-break (предпочитает tight-ЕКБ bbox) без города не различает одноимённые улицы внутри региона. 4 новых теста (C1×2, C2×2 + dedupe) — проверено что все 4 падают на коде до этого коммита через git stash (только geocoder.py, тесты оставлены). |
||
|
|
23b8816863 |
fix(tradein/estimator): гео-ограничение якорного тира — не брать чужие города (#2581)
All checks were successful
CI / changes (pull_request) Successful in 9s
CI Trade-In / changes (pull_request) Successful in 9s
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 2m13s
Tier A (_fetch_anchor_comps, "тот же дом") матчил по normalized street+house (_normalize_building_key намеренно дропает город) БЕЗ единого гео-предиката — единственный запрос к listings в файле без ST_DWithin/города/house_id_fk/LIMIT. "Серов, ул. Ленина, 5" получал якорь по ЕКБ-объявлениям (~40 191 из ~40 200 активных листингов — ЕКБ) с самым доверенным тиром 'A', завышая цену в 4-5х; хуже — anchor_tier='A' блокировал честный #oblast-D deals-headline-fallback (гейт `anchor_tier is None`), так что область не могла получить даже резервную ДКП-оценку. Фикс мирроит уже одобренный geo-bound для Tier S ( |
||
|
|
4b1a282234 |
fix(tradein/geocoder): не подставлять Екатеринбург молча — явный city_hint (#2576)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 2m17s
Раньше _yandex_lookup/_yandex_suggest/_nominatim_suggest молча подставляли "Екатеринбург, " в запрос, если в адресе не было маркера города/области. Житель Нижнего Тагила, вводя «Ленина, 1», получал уверенно неверную цену по екатеринбургской улице Ленина (обе улицы называются одинаково) — фронт город вообще не передаёт. - geocode()/suggest() принимают опциональный city_hint: str | None; без него внешние тиры больше НЕ подставляют город, а bias (ll/spn) смещается на всю область (OBLAST66_VIEWBOX) вместо ЕКБ-центра. Явный маркер города в адресе или city_hint сохраняют прежнее поведение (ЕКБ-путь не деградирует). - GeocodeResult.city_ambiguous — честный флаг «город определил провайдер, а не пользователь» (не эвристика на корректность), проброшен в AggregatedEstimate.target_city_ambiguous (ephemeral, не персистится). - Cache-ключ geocode_cache учитывает city_hint (address|city=...) — без hint'а формат не меняется (backward-compat), с hint'ом разные города для одного текста адреса больше не делят одну запись. - API: /api/v1/geocode/lookup, /suggest и POST /trade-in/estimate получили опциональный city_hint — контракт не ломается (default None). 23 новых теста в test_geocoder_city_hint.py; проверено что они падают (ImportError на _cache_key) на коде до фикса через git stash. |
||
|
|
f44ed4043c |
feat(tradein/db): авто-refresh ценовых бэндов по городам + порог для малых городов (#2576)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 12s
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 2m21s
Миграция 178 засеяла deal_city_price_bands разово (N>=30 сделок), без scheduler'а на refresh. Город без строки падал на глобальный DEAL_MIN_PPM2=50000 (ЕКБ-калибровка) — для дешёвых городов области это не anti-outlier guard, а cut-off легитимного рынка (Североуральск median ~21.7k). Замер по прод-данным: 289 из 369 не-ЕКБ городов (1265 сделок) не имели строки и падали на ЕКБ-порог. - 194_deal_city_price_bands_tiers.sql — трёхуровневая схема (tier колонка): full (N>=30, own p1/p99, unchanged) / rough (N 10-29, own p1 floor + фикс. ceiling 800000) / region_fallback (N 1-9, pooled областной p1=15263 вместо ЕКБ-порога). Екатеринбург по-прежнему не в таблице — estimator fallback byte-identical. - 195_scrape_schedules_seed_deal_city_price_bands_refresh.sql — scrape_schedules row, окно 07:00-08:00 UTC (после rosreestr_dkp_import + asking_to_sold_ratio_refresh). - app/tasks/deal_city_price_bands_refresh.py — периодический re-derive (kit-scheduler, byte-identical 194 derivation), без DELETE (множество городов монотонно растёт). - app/services/product_handlers.py — регистрация Handler для нового source. Валидация: scratch-БД (syntax_check) в прод-контейнере, synthetic данные на границах тиров (N=9/10/29/30) + Екатеринбург/non-rosreestr/NULL exclusion, оба файла применены дважды (идемпотентность подтверждена), scratch-БД удалена. |
||
|
|
0835266516 |
feat(tradein/auth): auth-core — login/logout, sessions, dual-mode rbac (#2552)
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 / backend-tests (pull_request) Successful in 1m19s
Foundation для эпика #2549: session-cookie auth поверх legacy Caddy trusted-header. app.services.auth_session — CRUD для tradein_sessions (create/get/revoke) + get_user_by_username для password-логина; opaque secrets.token_urlsafe токены, sliding last_seen_at/expires_at refresh (не чаще раза в 5 минут). POST /api/v1/auth/login проверяет password_hash (bcrypt) через app.core.password, ставит httponly+secure cookie, пишет login_success/login_failed в user_events; per-username+IP rate-limit (SlidingWindowLimiter) отдельно от общего RateLimitMiddleware. POST /logout ревокает сессию и чистит cookie. Оба пути exempt из rbac_guard's auth-required gate (иначе логин сам себя не пропустил бы). rbac_guard теперь dual-mode: session-cookie резолвится первым (DB-роль employee/manager/admin -> paths как у pilot/+team/admin), fallback на legacy X-Authenticated-User + roles.yaml БЕЗ ИЗМЕНЕНИЙ когда auth_mode == "dual"; auth_mode == "db_only" отключает legacy header полностью. Резолвленный сессией username инжектится в ASGI scope headers (до call_next) — RequestAuditMiddleware и downstream route-хендлеры видят его прозрачно; RateLimitMiddleware (внешний относительно rbac_guard) для session-запросов лимитирует по IP, не по username — документированный trade-off, не регрессия. GET /me — session-first: валидная cookie отдаёт scope из tradein_users без похода в roles.yaml; без cookie — прежний legacy путь. session_secret остаётся опциональным (opaque-токены не требуют подписи) — пустое значение только logger.warning на старте, не startup-fail. Полный набор тестов (tests/test_rbac.py, test_internal_auth_secret.py, test_account_quota.py) проходит без правок — regression-safe. |
||
|
|
5626d9e720 |
feat(mera/b2c): правовая рамка — согласие до сохранения, удаление по сроку и по запросу (этап 4 из 8)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
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 / 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 1m11s
Три дефекта, каждый блокировал легальный публичный запуск. 1. Адрес физлица сохранялся в базу ДО любого согласия: согласие фиксировалось только на форме заявки, то есть ПОСЛЕ записи адреса. Для пилота с договором терпимо, для человека с улицы — нет. Проверка согласия поставлена первой строкой расчёта, до геокодирования и до обоих мест записи адреса. Хранение — колонками на самой оценке, 1:1 с уже работающим прецедентом для заявок (миграция 182): IP клиента, версия политики, дословный снимок текста. Отдельная таблица событий не заводилась: согласие даётся ровно на создание этой строки, и когда строка удаляется по сроку, исчезновение доказательства вместе с данными логично. Enforcement НЕ выводится из пустого created_by — первая версия так и делала и сломала 92 несвязанных теста оценщика, которые зовут расчёт без имени пользователя, проверяя ценовую логику. Вместо этого явный флаг, который выставляет единственный боевой вызывающий. B2B-поток не тронут: поле согласия опционально, иначе сломались бы пилоты, чей фронт его не шлёт. 2. Срок жизни оценки применялся только как фильтр при чтении — физического удаления не было ни в одной фоновой задаче, данные жили вечно вопреки декларированному сроку. Заведена задача удаления пачками с ограничением на прогон и коммитом после каждой пачки, идемпотентная. В расписании она ВЫКЛЮЧЕНА: это первая автоматическая задача, удаляющая персональные данные, и первый прогон должен быть под наблюдением. 3. Пути «удалите мои данные» не было. Добавлен сервис удаления и админская ручка. Ключи: имя пользователя, идентификатор оценки, телефон, чат в телеграме. Честно зафиксировано в коде: аноним без ссылки на оценку, без оставленного телефона и без обращения в поддержку неидентифицируем — удалить его данные без дополнительной идентификации нельзя. Отдельно: удаление чистит только копию в базе, зеркало переписки в телеграм-топике не удаляется ничем в кодовой базе, нужен ручной шаг. 4. Соответствие текста согласия на фронте и снимка на бэке держалось на комментарии. Теперь есть тест, который ловит расхождение. Сроки хранения вынесены в настройки. Значение для заявок предложено инженерно (типичный отраслевой диапазон), юридически обоснованный срок — за юристом, и это записано в коде. Тесты: 2775 passed. |
||
| 5f3579b8c2 |
feat(tradein/auth): восстановить доступ praktika + безлимит через данные вместо хардкода (#2544)
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 1m2s
Deploy Trade-In / build-backend (push) Successful in 5m35s
Deploy Trade-In / deploy (push) Successful in 1m14s
|