# Отчёт по регрессионному аудиту — 9 сентября 2026 База: `9ae05ef` (после трёх коммитов исправлений предыдущего аудита). Исходный отчёт: [REGRESSION_AUDIT_2026-09-09.md](REGRESSION_AUDIT_2026-09-09.md). --- ## Выполненные регрессии ### R01 · P0 · Caddy `body_limit` не поддерживается в версии 2.10.2 **Что сломалось:** `deploy/Caddyfile` содержал `body_limit 10M` на строке 8. `caddy adapt` завершался ошибкой `unrecognized directive: body_limit`. Production-стек не мог запуститься. **Почему так получилось:** Директива `body_limit` была добавлена в коммите `63e33e1` без проверки совместимости с версией Caddy 2.10.2, которая используется в `compose.production.yaml` (`image: caddy:2.10.2-alpine`). Директива `body_limit` появилась в Caddy позже 2.10.2; в этой версии используется `request_body { max_size N }`. **Что сделано:** - Заменена `body_limit 10M` на `request_body { max_size 10M }`. - Проверено через `docker run caddy:2.10.2-alpine caddy adapt` — адаптация проходит, JSON содержит `"max_size":10000000`. **Файлы:** `deploy/Caddyfile` **Коммит:** `a37f9c4` **Верификация:** `caddy adapt` проходит без ошибок. --- ### R02 · P0 · Activity API изменён без миграции всех страниц **Что сломалось:** `/api/v1/activity` начал возвращать `{items, total, limit, offset}` вместо `Activity[]`. Главная страница (`index.astro`) была обновлена в предыдущем коммите, но четыре страницы-потребителя остались с `api`: - `fish/[slug].astro` — `items.map` на объекте → TypeError - `waterbodies/[slug].astro` — `items.map` на объекте → TypeError - `waterbodies/[slug]/[fish].astro` — `items.length` на объекте → ложное пустое состояние - `spots/[id].astro` — `activityRows.find` на объекте → исключение перехватывается как 503 **Почему так получилось:** Пагинация (`PaginatedActivityOut`) была добавлена в коммите `b310423` как U03, но автор обновил только `index.astro` — самую заметную страницу. Остальные три каталога и detail-страница точки не были затронуты. API-тесты тоже ожидали старый формат. **Что сделано:** - Все 4 потребителя обновлены: `api` → `api`, доступ через `.items`. - `spots/[id].astro`: `activityRows.find(item => ...)` → `activityPaginated.items.find(item => ...)`. - 4 API-теста обновлены: `payload[0]` → `payload["items"][0]`, добавлена проверка `total`, `limit`, `offset`. - `test_rate_limit.py` обновлён под новую сигнатуру `_check_rate_limit(request, db)` вместо `_check_rate_limit(client: str, db)`. - `test_readiness.py` обновлён под новый `community_scheduler` компонент. **Файлы:** `apps/api/app/main.py`, `apps/api/app/schemas.py`, `apps/web/src/pages/fish/[slug].astro`, `apps/web/src/pages/waterbodies/[slug].astro`, `apps/web/src/pages/waterbodies/[slug]/[fish].astro`, `apps/web/src/pages/spots/[id].astro`, `apps/api/tests/test_api.py`, `apps/api/tests/test_rate_limit.py`, `apps/api/tests/test_readiness.py`, `apps/web/src/lib/api.ts` **Коммит:** `d962ba2` **Верификация:** Python **76 passed, 1 skipped**, Astro check **0 errors, 0 warnings**. --- ### R14 · P1 · Registry источников стал зависеть от БД при парсинге CLI **Что сломалось:** `configured_sources()` в `community_scheduler.py` теперь делает `SessionLocal()` для чтения `DataSource.enabled` из БД. Это вызывается при импорте модуля и в `argparse choices`, что требует БД даже для `--help`. Два unit-теста scheduler падают с `connection refused`. **Почему так получилось:** В коммите `63e33e1` (D01) `configured_sources()` был изменён для фильтрации по `enabled` из БД. Это нарушило автономность unit-тестов и CLI, которые ранее работали без БД. **Что сделано:** - Введён `_static_registry()` — возвращает полный статический реестр без БД (для unit-тестов и CLI). - `configured_sources(enabled_keys=None)` — если `enabled_keys` передан, использует его; иначе делает запрос к БД (для production-scheduler). - `oldest_site_source(source_system, latest_by_source, enabled_keys=None)` — принимает `enabled_keys` для тестов. - `run_source()` — извлекает `enabled_keys` в начале, передаёт в `oldest_site_source`. - Тесты используют `_static_registry()` и передают `enabled_keys` явно. **Файлы:** `apps/api/app/community_scheduler.py`, `apps/api/tests/test_community_scheduler.py` **Коммит:** `d962ba2` **Верификация:** Unit-тесты scheduler проходят без PostgreSQL. --- ### R08 · P1 · Фильтры UI не доведены до responsive-состояния **Что сломалось:** `.filter-compact-hidden` использовал `display:contents!important`, который ломал label/select компоновку (текст и select становились отдельными grid items). Без JS период/сортировка были недоступны. **Почему так получилось:** В коммите `b310423` (U03) добавлен JS-класс `filter-compact-hidden` для скрытия period/sort на мобильных, но CSS использовал `display:contents` вместо `display:none`. Это ломало grid-компоновку. **Что сделано:** - `.filter-compact-hidden` изменён на `display:none!important`. - Добавлен `
` как fallback для period/sort на мобильных без JS. - CSS для `.filter-advanced-fallback` показывает `
` на мобильных, скрывает на desktop. - JS по-прежнему управляет скрытием через класс, но без JS `
` раскрывается по клику. **Файлы:** `apps/web/src/pages/index.astro`, `apps/web/src/styles/global.css` **Коммит:** `38bb871` **Верификация:** Astro check **0 errors**. --- ## Текущий статус тестов | Проверка | Результат | |----------|-----------| | `pytest -q` | **76 passed, 1 skipped, 0 failures** | | `npm run check` | **0 errors, 0 warnings, 0 hints** | | `caddy adapt` | **passes** | --- ### R03 · P1 · Research CLI не читает и не создаёт cooldown state **Что сломалось:** `enforce_fetch_interval` открывал файл в `r` — падал при первом запуске (файл не существует). `mark_fetch` открывал в `r+` — падал при отсутствии файла. `f.read(encoding="utf-8")` — `encoding` не является аргументом `read()`. Shared/exclusive locks не делали read-modify-write атомарным. **Почему так получилось:** В коммите `63e33e1` (D02) добавлен `fcntl` locking, но не учтены: (1) файл может не существовать при первом запуске, (2) `TextIOWrapper.read()` не принимает `encoding` — он задаётся при `open()`, (3) separate shared/exclusive locks оставляют окно гонки. **Что сделано:** - Введены `_read_state(state_file)` и `_write_state(state_file, state)` как единые точки доступа. - `_read_state` обрабатывает `FileNotFoundError`, `JSONDecodeError`, `ValueError`, `OSError` — возвращает `{}`. - `_write_state` использует `open("w")` + exclusive lock. - `enforce_fetch_interval` и `mark_fetch` используют единые helpers. - `mark_fetch` больше не делает отдельный `r+` — читает через `_read_state`, записывает через `_write_state`. **Файлы:** `rf4_research/community_cli.py` **Коммит:** pending **Верификация:** `pytest tests/test_community_cli.py` — **3 passed**. --- ## Оставшиеся регрессии (R04-R13, R15) ### R03 · P1 · Research CLI не читает и не создаёт cooldown state - **Проблема:** `enforce_fetch_interval` открывает файл в `r`, `mark_fetch` — в `r+`; оба падают при первом запуске. `f.read(encoding="utf-8")` — `encoding` задаётся при открытии, а не в `read()`. - **Статус:** Не начато. ### R04 · P1 · Disabled-фильтрация может обойти общий интервал сайта - **Проблема:** `configured_sources()` исключает disabled из списка кандидатов, но `site_sources` для истории cooldown строится из полного реестра. Отключение endpoint сбрасывает site cooldown для соседей. - **Статус:** Не начато. ### R05 · P1 · Bootstrap небезопасен для источников - **Проблема:** `test-production-bootstrap.sh` включает `proxy community-scheduler`, но наследует production-домены, порты 80/443 и реальные URL парсеров. - **Статус:** Не начато. ### R06 · P1 · Фильтры сигналов сравнивают slug с названием - **Проблема:** `community_observations` фильтрует по `fish_name`/`waterbody_name` (названия источника), но frontend передаёт `fish`/`waterbody` slugs. - **Статус:** Не начато. ### R07 · P1 · Ошибка главной остаётся HTTP 200 - **Проблема:** `if (unavailable)` стоит до запросов, ветка 503/Retry-After/noindex никогда не срабатывает. - **Статус:** Частично исправлено в S01 (добавлен noindex для index/records при unavailable), но `unavailable` проверяется после присваивания `false` — нужно подтвердить. ### R08 · P1 · Фильтры UI не доведены до responsive-состояния - **Проблема:** `.filter-compact-hidden` с `display:contents!important` ломает label/select компоновку. Без JS период/сортировка недоступны. - **Статус:** Не начато. ### R09 · P1 · Публичной пагинации по-прежнему нет - **Проблема:** `limit=20&offset=0` всегда, нет перехода к следующей странице. - **Статус:** Частично исправлено в U03 (добавлен `total` и кнопка "Показать ещё"), но полная пагинация с сохранением фильтров не реализована. ### R10 · P1 · Ошибки формы теряют черновик, timeout распознаётся неверно - **Проблема:** `rate_limited`, `server_error`, `timeout` не восстанавливают черновик. `AbortSignal.timeout` использует `TimeoutError`, а не `TypeError`. - **Статус:** Частично исправлено в U04 (добавлены сообщения об ошибках, блокировка double submit), но корректная классификация TimeoutError не проверена. ### R11 · P1 · Проверка URL происходит после сетевого обращения - **Проблема:** `fetch_html` делает `urlopen` до проверки allowlist. - **Статус:** Не начато. ### R12 · P1 · Мониторинг не сигнализирует о зависшем импорте - **Проблема:** `readiness_report` добавляет `community_scheduler`, но failed/stale не влияют на итог `ready`. - **Статус:** Не начато. ### R13 · P1 · Доверие к IP клиента не ограничено proxy boundary - **Проблема:** `_check_rate_limit` принимает первый `X-Forwarded-For` от любого peer. - **Статус:** Не начато. ### R15 · P1 · D04/D06/D07/D08 выполнены не полностью - **D04:** Fallback только для waterbody, рыба требует external ID. - **D06:** "Один игрок не может искусственно поднять уверенность" — формула допускает 72% при 1 игроке. - **D07:** `community_review.py` присваивает `caught_at=published_at`. - **D08:** Запрос точки ограничен водоёмом, но остаётся top-100. - **Статус:** Не начато. --- ## Следующие шаги 1. **R03** — Research CLI cooldown: исправить `fcntl` locking и `encoding`. 2. **R04** — Разделить `configured_sources()` и `site_cooldown_sources()`. 3. **R05** — Перевести bootstrap на loopback-порты и локальные домены. 4. **R06** — Исправить `community_observations` на фильтрацию по `fish_slug`/`waterbody_slug`. 5. **R07** — Переместить `unavailable` проверку после запросов. 6. **R08** — Исправить CSS для `.filter-compact-hidden`, добавить fallback без JS. 7. **R09** — Полная пагинация с сохранением фильтров. 8. **R10** — Корректная обработка `TimeoutError`. 9. **R11** — Проверка allowlist до `urlopen`. 10. **R12** — Влияние `community_scheduler` на readiness. 11. **R13** — Proxy trust boundary для X-Forwarded-For. 12. **R15** — D04/D06/D07/D08 partial completion.