Files
rf4-spotter/docs/REGRESSION_AUDIT_2026-09-09.md

145 lines
24 KiB
Markdown
Raw Permalink 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
## Вывод и границы проверки
Текущий `9ae05ef` не готов к production. Проверены изменения `c6fdc96..9ae05ef`: три коммита, 19 файлов, 208 добавленных и 80 удалённых строк. Изменений тестов, README и ROADMAP в этом диапазоне нет. Поэтому по доступной ветке нельзя подтвердить выполнение всего предыдущего аудита: часть исправлений полезна, часть неполна, есть новые блокирующие регрессии.
Это проверка кода и локальных команд, а не повторная визуальная приёмка в браузере или проверка рабочего сервера. Парсеры в сеть не запускались: разрешённый интервал не менее 30 минут на площадку сохраняется. Production/bootstrap не запускался; для Caddy использован только одноразовый контейнер без сети. Неотслеживаемые `.gigacode/`, `.gigaide/`, `.idea/` не изменялись.
### Фактические проверки
| Проверка | Результат |
| --- | --- |
| `.venv/bin/pytest -q --tb=short` | **10 failed, 86 passed, 1 skipped**, 3,54 с |
| `.venv/bin/pytest -q tests/test_community_cli.py --tb=short` | **2 failed, 1 passed** |
| `npm --prefix apps/web run check` | 40 файлов, 0 ошибок/предупреждений |
| `npm --prefix apps/web run test:unit` | 1 успешный test-file subtest |
| `npm --prefix apps/web run build` | Успех |
| Caddy 2.10.2, `caddy adapt` с текущим Caddyfile | **Ошибка: строка 8, unrecognized directive: body_limit** |
Из 10 падений Python: два относятся к research cooldown; четыре — к изменённому activity-контракту; два — к новой зависимости scheduler helpers от внешней БД; по одному — к изменению сигнатуры rate limit и состава readiness. Последние два сами по себе не доказывают поломку функциональности: старые проверки не обновлены под новый контракт. Но зелёной приёмки изменений нет. Успешная сборка Astro не проверяет соответствие JSON реальному типу `api<T>`.
## Подтверждённые регрессии и недоработки
### R01 · P0 · Production proxy не может запуститься
- Доказательство: [Caddyfile](../deploy/Caddyfile), строка 8, `body_limit 10M`; локальный `caddy adapt` завершается ошибкой неизвестной директивы.
- Последствие: недоступны сайт, API и файловый домен при запуске с этим конфигом. Прежняя успешная проверка маршрутизации не подтверждает текущий HEAD.
- Исправление: поддерживаемая конфигурация лимита тела, затем проверка конфигурации и маршрутов; проверить 413 для JSON и multipart. Не ограничиться удалением строки: требование защиты от больших запросов остаётся.
### R02 · P0 · Activity API изменён без миграции всех страниц
- [main.py](../apps/api/app/main.py): `/api/v1/activity` теперь возвращает `{items,total,limit,offset}`. На новый формат переведена только главная.
- [fish/[slug].astro](../apps/web/src/pages/fish/[slug].astro) и [waterbodies/[slug].astro](../apps/web/src/pages/waterbodies/[slug].astro) вызывают `items.map` вне `try` у объекта, а не массива: путь рендера заканчивается TypeError.
- [waterbodies/[slug]/[fish].astro](../apps/web/src/pages/waterbodies/[slug]/[fish].astro) проверяет `items.length` у объекта и выбирает ложное пустое состояние.
- [spots/[id].astro](../apps/web/src/pages/spots/[id].astro) вызывает `activityRows.find` у объекта; исключение перехватывается как 503, запрос timeline не выполняется.
- Четыре существующих API-теста падают после смены контракта. Перечисленные последствия страниц установлены по коду; отдельный SSR-прогон ещё нужен.
- Приёмка: один общий контракт и все потребители, SSR для четырёх маршрутов с данными и пустым `items`, полная пагинация без потерь; старый контракт либо явно мигрирован, либо сохранена совместимость.
### R03 · P1 · Research CLI не читает и не создаёт cooldown state
- [community_cli.py](../rf4_research/community_cli.py): `enforce_fetch_interval` открывает отсутствующий файл в `r`, `mark_fetch` — в `r+`; оба падают при первом запуске. Подтверждено двумя тестами.
- Для существующего файла `f.read(encoding="utf-8")` тоже ошибочен: encoding задаётся при открытии, а не в `TextIOWrapper.read`.
- Раздельные shared/exclusive lock не делают проверку и резервирование атомарными. Разблокировка перед закрытием/flush записи оставляет дополнительное окно гонки; миграции старых ключей нет.
- Приёмка: первый запуск, существующий и повреждённый state, миграция ключей, конкурентные процессы, неудачный HTTP расходует интервал. Production и исследовательский путь не должны обходить общий лимит.
### R04 · P1 · Disabled-фильтрация может обойти общий интервал сайта
- [community_scheduler.py](../apps/api/app/community_scheduler.py): `configured_sources()` теперь оставляет только enabled. Из этого списка строится и `site_sources` для поиска недавних попыток.
- Сценарий по коду: RF4-STAT fishing только что запрошен → его отключают → posts больше не учитывает fishing в истории сайта и может запросить тот же сайт раньше 1800 секунд. Исключать отключённые endpoint нужно из кандидатов, **не из истории площадки**.
- `run_source(disabled)` теперь может получить KeyError до прежнего безопасного `return False`; повторные снимки registry открывают гонку при переключении enabled.
- Приёмка: выключение/включение endpoint не сбрасывает site cooldown; конкурентные запуски видят общую историю и только актуальных кандидатов.
### R05 · P1 · Bootstrap перестал быть изолированным и безопасным для источников
- [test-production-bootstrap.sh](../deploy/test-production-bootstrap.sh) добавляет production `proxy community-scheduler`, но [compose.bootstrap.yaml](../deploy/compose.bootstrap.yaml) переопределяет только API/web.
- Наследуются host-порты 80/443, production домены/TLS и реальные URL парсеров. Свежая БД bootstrap не знает историю запросов другого стека; повторные проверки способны нарушать 30-минутное ограничение. Импорт также конфликтует с проверкой `catch_report = 0`.
- Проверки curl/Playwright продолжают ходить напрямую на API/web, а не через новый proxy: запуск контейнера не равен проверке маршрутизации.
- Приёмка: loopback high ports, локальные HTTP-домены, fixture HTTP-источник и запрещённый внешний egress; сценарии формы/модерации проходят через Caddy. Реальные источники — только отдельный согласованный smoke с общей историей интервалов.
### R06 · P1 · Фильтры сигналов сравнивают slug с названием
- [index.astro](../apps/web/src/pages/index.astro) отправляет выбранные slug `fish`/`waterbody`.
- [main.py](../apps/api/app/main.py), `community_observations`, сравнивает их с `fish_name`/`waterbody_name` (названия источника). Наблюдение «Щука» не совпадает с `pike` и исчезает при фильтрации.
- Приёмка: единая семантика slug/ID с подтверждёнными mapping/alias; отдельно определить поведение несопоставленных наблюдений. Проверить источник, неполноту и выбранные фильтры вместе, не теряя атрибуцию.
### R07 · P1 · Ошибка главной остаётся HTTP 200 и индексируемой
- [index.astro](../apps/web/src/pages/index.astro): `if (unavailable)` находится **до** запросов, сразу после присвоения `false`; в `catch` меняется только флаг. Ветка 503/Retry-After/noindex никогда не срабатывает при сбое API.
- На detail-страницах `noindex` зависит от найденной сущности, не от `unavailable`: при успешном справочнике и сбое activity ошибочная страница не закрывается через этот флаг.
- Приёмка: после завершения загрузки согласовать status, noindex, Cache-Control, Retry-After и JSON-LD для 200-empty/404/422/503; аварийная страница не изображает полноценный Dataset.
### R08 · P1 · Фильтры UI не доведены до рабочего responsive-состояния
- [global.css](../apps/web/src/styles/global.css): `.filter-advanced-field{display:contents}` применяется уже к `label`: текст подписи и select теряют общий grid-контейнер и становятся отдельными grid items.
- На mobile поля скрыты CSS, но JS добавляет класс `filter-compact-hidden`, который задаёт противоположное — `display:contents!important`. Управления раскрытием больше нет. Без JS период и сортировка недоступны.
- [index.astro](../apps/web/src/pages/index.astro): только вариант 24 часов имеет `selected`; после запроса 6/12/72 форма показывает первый вариант вместо фактического фильтра и может менять период при повторной отправке.
- Приёмка: связная label/select компоновка, сохранённые выбранные значения, доступные все фильтры с JS и без него; desktop/mobile, клавиатура и zoom. Это статические находки; визуальную приёмку нужно выполнить после исправления R02.
### R09 · P1 · Публичной пагинации по-прежнему нет
- [index.astro](../apps/web/src/pages/index.astro) запрашивает всегда `limit=20&offset=0`, добавлен total, но нет перехода к следующей странице активности.
- Рекорды/каталоги также не получили полноценную публичную навигацию по страницам в проверенном diff. Рост лимита полевых сигналов до 48 не решает задачу остальных списков.
- Приёмка: 21+ результатов достижимы, фильтры сохраняются, счётчик различает показанное/общее; определить SEO-политику пагинации.
### R10 · P1 · Ошибки формы теряют черновик, timeout распознаётся неверно
- [report.astro](../apps/web/src/pages/report.astro) восстанавливает черновик и фокус только при `create_error`, но не при новых `rate_limited`, `server_error`, `timeout`.
- [api/report.ts](../apps/web/src/pages/api/report.ts) ищет timeout как `TypeError` с текстом `abort`; `AbortSignal.timeout` использует TimeoutError, поэтому новое состояние обычно недостижимо.
- Отключение кнопки не даёт серверной идемпотентности: при таймауте создания неизвестно, сохранил ли API заявку; повтор может создать дубль. Обработка multipart retry-маршрута и единая обработка 413 также требуют завершения.
- Приёмка: восстановление полей/фокуса при каждом отказе, безопасное поведение при недоступном sessionStorage, корректная классификация timeout и повтор без дублирования сохранённого улова.
### R11 · P1 · Проверка URL происходит после сетевого обращения
- [community_cli.py](../rf4_research/community_cli.py), `fetch_html`: сначала `urlopen`, затем allowlist конечного `response.url`. Запрос к запрещённому initial URL или redirect уже произошёл до отказа.
- Лимит чтения 5 МБ полезен, но не исправляет эту проблему. `fetch_site_key` не объединяет `rf4db.com` и `download.rf4db.com` в площадку.
- Приёмка: validate scheme/host/port до I/O и на каждом redirect, общий ключ площадки, ограничение ответа; тесты только на mock/fixture transport.
### R12 · P1 · Мониторинг не сигнализирует о зависшем импорте
- [readiness.py](../apps/api/app/readiness.py) добавляет community-компонент, но его failed/stale/unknown не влияют на итог `ready`. Выбирается одна последняя попытка всех источников: успех одного маскирует проблемы других.
- [monitor.sh](../deploy/monitor.sh) проверяет лишь running контейнер и наличие текста `"status":"ready"` где-либо в JSON. Он не проверяет component status/свежесть каждого источника.
- Приёмка: разделить готовность API обслуживать данные и здоровье сбора, но обязательно выдавать мониторинговую тревогу по enabled source/site с учётом ротации и backoff. Проверить running без завершения, stale и failed при исправных DB/MinIO.
### R13 · P1 · Доверие к IP клиента не ограничено proxy boundary
- [main.py](../apps/api/app/main.py), `_check_rate_limit`: первый `X-Forwarded-For` принимается от любого peer без проверки доверенного прокси и валидности IP.
- [api/report.ts](../apps/web/src/pages/api/report.ts) при отсутствии заголовков передаёт общий `unknown`. Это вновь общий bucket для прямого доступа к Astro.
- Публичный обход через штатный Caddy здесь **не доказан**: proxy может очищать входной XFF. Подтверждено отсутствие защиты на уровне API и зависимость от внешнего trust boundary.
- Приёмка: определить допустимые peer/proxy, проверять IP/цепочку, доказать независимость двух клиентов через proxy и игнорирование подделанного XFF на недоверенном входе.
### R14 · P2 · Registry источников стал зависеть от БД при разборе CLI
- [cli.py](../apps/api/app/cli.py) вызывает `configured_sources()` при построении argparse `choices`, до разбора команды, включая `--help`. Теперь это открывает внешнее соединение с БД.
- Два прежних scheduler unit-теста падают с connection refused к localhost:5432: helpers перестали быть автономными.
- Приёмка: отделить статический registry от выборки enabled в переданной сессии; `--help` работает без БД; unit-тесты не требуют поднятого PostgreSQL, отдельная интеграционная проверка покрывает конкуренцию.
### R15 · P1 · Заявленные D04/D06/D07/D08 выполнены не полностью
- D04: [community_importer.py](../apps/api/app/community_importer.py) добавляет fallback только водоёма по `Waterbody.name_ru`; рыба по-прежнему требует external ID и alias. `review_note` говорит о проверенных алиасах даже при новом name fallback. Нужны единые правила и точное объяснение автоматического соответствия.
- D06: на главной остаётся «Один игрок не может искусственно поднять уверенность». Формула [activity.py](../apps/api/app/activity.py) при 10 отчётах одного игрока с source confidence 100 даёт 72%: 45 + 7 + 20. Ограничения вклада по-прежнему нет.
- D07: замена `last_confirmed_at` на `reported_at` не разделяет все события. [community_review.py](../apps/api/app/community_review.py) всё ещё присваивает `caught_at=published_at`. Неизвестное время улова нельзя выдавать за известное.
- D08: запрос точки теперь ограничен водоёмом, но остаётся top-100; поиск только по spot_id не различает виды рыбы. После R02 нужен адресный агрегат/явный список видов на точке, а не поиск внутри страницы рейтинга.
- Приёмка: fixture-набор с отсутствующим ID, публикацией позже улова, неизвестным временем, одним игроком и несколькими рыбами одной точки; публичные подписи соответствуют фактическим данным.
## Что полезного уже добавлено
- Community scheduler подключён к `backend` и `edge` в production Compose (направление T03 верное; egress-проверка текущего стека ещё нужна).
- Публичный `/imports` использует отдельный DTO без диагностических подробностей (T07; добавить regression-проверку полей).
- Введены лимит ответа источника 5 МБ, таймауты POST, отдельные сообщения 429/5xx, счётчик activity total и 503/noindex для рекордов. Это частичные улучшения, не основание закрывать связанные задачи целиком.
- Прежние исправления разделения Astro/API и Basic/Bearer сохранены в коде, но новый невалидный Caddyfile блокирует их применение.
## План исправления и дальнейших работ
Рабочие чекбоксы находятся в [ROADMAP](ROADMAP.md#повторная-приёмка-9-сентября-2026). Порядок: R01 → R02 → R03/R04/R05/R11 → R06R10 → R12R15. Не нужно перезапускать Docker после каждой правки.
1. Восстановить deploy и контракт страниц. Адресные offline-тесты + один Caddy adapt.
2. Устранить риски интервала и произвольных HTTP-запросов. Bootstrap перевести на fixtures до его следующего запуска.
3. Закрыть пользовательские/SEO-регрессии и правдивость данных. Затем единая SSR/браузерная матрица с наполненными страницами.
4. Восстановить зелёный Python suite, расширить web tests проверками потребителей API и добавить их в CI. Не подменять исправление регрессий ослаблением assertions.
5. Один изолированный production acceptance: Caddy → Astro → API, форма → модерация → публикация с обязательным источником, неполные сигналы и мониторинг. Без живого scraping.
6. Продолжить незавершённый предыдущий аудит: D05/S02 (полный каталог, lookup по slug, очередь detail URL); V01V04/U05U07 (идентичность, токены, плотность карточек, координаты, иконки, визуальная приёмка); S03S06 (canonical, полезные постоянные страницы, sitemap, performance); T08T10/D09D10 (воспроизводимость, нагрузка, release/migration, ограниченные права хранения, история импорта/backoff).
7. После сервера: внешний launch checklist, S07; после обратной связи — V05. Astro/FastAPI/PostgreSQL сохраняются: смена стека эти дефекты не исправит.
Каждый пункт закрывается только с доказательством своей приёмки, обновлением README при изменении поведения и отдельным коммитом. Старые отметки о завершении относятся к прежним ревизиям, а не автоматически к текущему HEAD.