@app.get("/health") в FastAPI/Starlette не добавляет HEAD-обработчик
автоматически (в отличие от низкоуровневого Route(methods=["GET"])) —
внешний uptime-monитор (GlitchTip PING-тип шлёт HEAD) получал 405 и не
мог отличить "жив" от "мёртв" по статусу. Добавлен явный
@app.head("/health") — 200 без тела (RFC 9110 §9.3.2), GET не тронут.
Тест test_health_endpoint.py фиксирует оба метода; RED до фикса
(HEAD → 405), GREEN после (проверено git stash + повторный прогон).
Revisit-floor (#2659) raises effective TTL via max(ttl_days, floor) with no
upper bound -- a positive feedback loop confirmed on prod: slow crawl raises
the floor, a high floor keeps stale listings marked active longer than a
fresh sweep needs to return, the "active" pool bloats with rot, and the next
floor measurement on that bloated pool comes out even higher. Yandex counters
sat at ttl_days_effective=75/75/75/39/52/54 for six runs straight with
deactivated=0; 23,687/44,744 "active" avito listings hadn't been confirmed in
>7 days, cian 10,572/19,514 and yandex 7,178/15,790 were >30 days stale, the
oldest "active" row hadn't been seen in 86 days.
CAP_MULT=2 caps the floor's upward push without disabling it -- the floor
still protects against premature deactivation during genuinely slow (but
alive) crawl cycles, it just can no longer grow unbounded. Beyond 2x, a
persistently low crawl rate is better handled by the existing health gate
(min_confirmations), which disables deactivation outright instead of
stretching TTL forever.
When the cap binds, counters gain ttl_floor_capped=1 + ttl_days_floor_raw
(the uncapped value) so it's visible in the run-history dashboard, not just
logs -- counters are stored as-is in scrape_runs.counters.
Single fix point: all four sources (avito/yandex/cian/domklik) route through
this one deactivate_stale_listings() via the product_handlers wildcard
"deactivate_stale_*" handler, so no other task file needed the change.
РКН/владелец: рядом с чекбоксом согласия должна быть ссылка на сам документ
политики обработки ПДн, а не упоминание закона. Чекбокс в LeadForm.tsx
(v2, живой /trade-in/v2) теперь линкует "Политикой обработки персональных
данных" на /mera-public/privacy (target=_blank, чтобы не терять заполненную
форму). Путь вынесен в новый src/lib/legal-copy.ts (модуль без импортов) —
content.ts ре-экспортирует оттуда, чтобы B2B-виджет не тянул B2C-лэндинг-модуль
целиком.
_CONSENT_TEXT_SNAPSHOT/_CONSENT_POLICY_VERSION в lead.py обновлены под новый
плоский текст и дату утверждения политики (PRIVACY_APPROVAL: 2026-08-13).
test_consent_text_frontend_sync.py: экстрактор теперь снимает JSX-теги/{" "}
спейсеры перед сравнением (иначе сломался бы на разметке ссылки) + новый тест
держит _CONSENT_POLICY_VERSION в синхроне с PRIVACY_APPROVAL из content.ts,
чтобы версия не расходилась молча с редакцией документа.
Легаси-дубль в HeroTransparency.tsx (недостижим с живого роута) — текст
приведён в соответствие без ссылки: компонент не смонтирован нигде, и нет
теста, который держал бы там ссылку в актуальном состоянии.
price_index нормирован на медиану Екатеринбурга (99a_quarter_price_index.sql),
поэтому фолбэк `avg_analog_index = ... else 1.0` подставлял в знаменатель
gap-коррекции не «нейтраль», а уровень ЕКБ. Для цели вне ЕКБ (индексы области
0.28–0.82) это превращало поправку в безусловную скидку: factor = target_qi,
после клампа до −40%, с подписью «Учтена локация квартала» — то есть догадка
выдавалась пользователю за методику.
Нет данных → нет поправки. Ровно тот же factor=1.0 получается из avg := target_qi,
и это лучшая оценка неизвестного avg на живых данных: медиана |ошибки| 0.116
против 0.161 у 1.0, p90 0.337 против 0.517 (400 лотов, 2026-08-12).
Проверка направления на сделках Росреестра (12 мес, медианы ₽/м² по городам):
без поправки ошибка +0…+14%, с текущей поправкой −32…−40%. Правка поднимает
цену и одновременно уводит её к правде, а не просто вверх.
MV и FDW не трогаются намеренно: строки basis='district'/'city_fallback' имеют
n_deals 3–4, а эстиматор требует n_deals >= 10 — второй 1.0 (city_fallback в
99a) до него структурно не доходит (прод: 0 из 1894 строк видимы).
Тесты: два прежних кейса задавали уровень аналогов отсутствием кадастра,
то есть опирались на сам дефект — переведены на явную карту analog_indexes.
Refs #2583
Авито с 27.07 рендерит рейтинг и число отзывов внутри того же <p> в
data-marker="item-location", откуда serp.py берёт адрес: «ул. Ткачей,17·5,0 · 4
отзыва». Прод 2026-08-10: 1 123 активных объявления с таким адресом, и у 1 123 из
1 123 нет координат — доля 100%, 711 из них геокодер уже пробовал. Контроль в тех
же данных: у чистых адресов координаты есть у 4 766 из 5 715 (83%).
Строка без geom молча выпадает из comp-пула: Tier W отбирает через ST_DWithin, а
NULL не проходит предикат и нигде не считается. 2 446 из 3 270 безкоординатных
попадают в свежий пул аналогов — 12.7% аналогов невидимы радиусному поиску.
Режем по «·», за которой идёт ЦИФРА (рейтинг «·4,9», счётчик «·2 отзыва»). По
любой «·» нельзя: разделитель района пишется «, 59 · р-н Академический» — за
точкой буква, и этот хвост _deglue_house_marker намеренно сохраняет (#1773).
Тест проверяет обе стороны плюс три прежних хвоста (CSS, метро, «от N мин.»).
Чинит только новые вставки: base.py пишет address = COALESCE(listings.address,
EXCLUDED.address), адрес при конфликте не перезаписывается осознанно (#2777).
Бэкфилл 1 123 существующих строк вынесен в #2814 для database-expert.
Во втором коммите PR отдельный `git fetch origin main` был снят (из
job-контейнера git.gendsgn.ru недостижим, run 6977), эталон даёт сам checkout с
fetch-depth: 0. Комментарий про «тянут main отдельным шагом» остался и читался
бы дальше как факт.
Конфликт modify/delete по tradein-mvp/backend/data/sql/_manifest_applied.txt
разрешён удалением — удаление и есть предмет PR. За трое суток в main дописали
три имени (240/250/251); дописывать их некуда: файла больше нет, а гейт
tests/test_migration_numbering.py берёт эталон применённого из origin/main.
Проверено, что удаление ничего не оставляет без потребителя: манифест не *.sql,
цикл миграций в deploy-tradein.yml и bootstrap схемы в ci-tradein.yml берут
glob '*.sql', новый scripts/check-migration-lock-timeout.py — тоже.
Замер дрейфа на 27e199e3: 216 имён в манифесте против 232 файлов, отставание
16 (было 15 на 07.08). Старый гейт на этом дереве: 4 passed.
# Conflicts:
# tradein-mvp/backend/data/sql/_manifest_applied.txt
PR-D2 платёжного контура МЕРЫ — закрывает утечки до открытия публичных путей
(PR-D3/D4), сам ничего не открывает: _PUBLIC_PATHS (rbac.py), Caddyfile,
roles.yaml, auth_session.py не тронуты.
- sentry_scrub.py: новая scrub_payment_request_body — вырезает
event.request.data целиком для /api/v1/trade-in/payments/* (sentry_sdk 2.64
кладёт полное тело запроса в request.data, send_default_pii=False это НЕ
гейтит — тот флаг управляет только куками). Плюс расширен _PII_KEYS:
customer_email/customer_phone/pan/expdate/cardid/rebillid/token/terminalkey.
- main.py, scheduler_main.py, tgbot_main.py (все 3 точки инициализации
sentry_sdk.init в проекте) — тот же обработчик проведён в ОБА канала,
before_send и before_send_transaction. Мотивирующий инцидент: на соседнем
продукте вчера закрыли только error-канал, transaction остался без
обработчика вообще.
- ratelimit.py: точный путь notify — свой щедрый SlidingWindowLimiter
(3000/60с per-IP, идиома support.py) вместо общего лимитера, но НЕ полное
отключение — backstop против шторма запросов остаётся, подпись проверяется
уже после разбора тела (PR-D3). Только notify, не checkout (тот с сессией).
- request_audit.py: notify — в audit skip-набор (defense-in-depth: middleware
внешний относительно rbac_guard и читает сырой X-Authenticated-User —
спуфнутый заголовок иначе писал бы фальшивые события с атрибуцией admin).
- smoke-mera-perimeter.sh: негативные проверки-канарейки — notify/checkout
сейчас закрыты 404 (meraocenka.ru, Caddy не проксирует) и 401
(gendsgn.ru, rbac ещё не открыл) с обеих сторон периметра.
Тесты: scrub на произвольной глубине + payment-path body-wipe, AST-разбор
(не substring — комментарии в этих же файлах сами упоминают
before_send_transaction) на проводку обоих каналов во всех точках
инициализации, 400 запросов notify без единого 429 + контроль что общий
лимитер по-прежнему активен на других путях, notify вне user_events даже со
спуфнутым X-Authenticated-User: admin.