Files
rf4-spotter/docs/REGRESSION_FIXES_REPORT.md
T
ik 45b73ec54a
CI / backend-and-migrations (push) Canceled after 0s
CI / astro-build (push) Canceled after 0s
CI / dependency-audit (push) Canceled after 0s
CI / compose-e2e (push) Canceled after 0s
docs: refresh roadmap after recovery acceptance
2026-09-11 08:06:40 +07:00

186 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Отчёт по регрессионному аудиту — 9 сентября 2026
> **Архивный промежуточный отчёт.** Его раздел «оставшиеся регрессии» закрыт последующим пакетом восстановления. Актуальный итог находится в [RECOVERY_FIXES_REPORT.md](RECOVERY_FIXES_REPORT.md), текущие задачи — в [ROADMAP.md](ROADMAP.md).
База: `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<Activity[]>`:
- `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<Activity[]>``api<PaginatedActivity>`, доступ через `.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`.
- Добавлен `<details class="filter-advanced-fallback">` как fallback для period/sort на мобильных без JS.
- CSS для `.filter-advanced-fallback` показывает `<details>` на мобильных, скрывает на desktop.
- JS по-прежнему управляет скрытием через класс, но без JS `<details>` раскрывается по клику.
**Файлы:** `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.