# Повторный аудит после исправлений — 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`. ## Подтверждённые регрессии и недоработки ### 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 → R06–R10 → R12–R15. Не нужно перезапускать 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); V01–V04/U05–U07 (идентичность, токены, плотность карточек, координаты, иконки, визуальная приёмка); S03–S06 (canonical, полезные постоянные страницы, sitemap, performance); T08–T10/D09–D10 (воспроизводимость, нагрузка, release/migration, ограниченные права хранения, история импорта/backoff). 7. После сервера: внешний launch checklist, S07; после обратной связи — V05. Astro/FastAPI/PostgreSQL сохраняются: смена стека эти дефекты не исправит. Каждый пункт закрывается только с доказательством своей приёмки, обновлением README при изменении поведения и отдельным коммитом. Старые отметки о завершении относятся к прежним ревизиям, а не автоматически к текущему HEAD.