diff --git a/docs/RECOVERY_FIXES_REPORT.md b/docs/RECOVERY_FIXES_REPORT.md new file mode 100644 index 0000000..615a217 --- /dev/null +++ b/docs/RECOVERY_FIXES_REPORT.md @@ -0,0 +1,214 @@ +# Отчёт по регрессионному аудиту — 10 сентября 2026 + +База: `9ae05ef`. План восстановления: [RECOVERY_PLAN_2026-09-10.md](RECOVERY_PLAN_2026-09-10.md). + +--- + +## Выполненные регрессии (A01–A10) + +### A01 · P0 · Убрать зависимость восстановления импорта от его свежести + +**Что сломалось:** `readiness.py` возвращал `ready=False` для stale/failed импортов. Production scheduler не мог запуститься, пока API не ready — цикл "курица и яйцо". + +**Что сделано:** +- Infrastructure (DB/MinIO) блокирует readiness; импорты — только диагностические сигналы +- Per-source community scheduler health с backoff detection +- Stale/failed импорты больше не блокируют `/ready` +- Добавлен `blocking: false` ко всем import-компонентам +- 4 новых теста: per-source health, backoff detection, stale/failed non-blocking + +**Файлы:** `apps/api/app/readiness.py`, `apps/api/tests/test_readiness.py` +**Коммит:** `779d554` +**Верификация:** Python **108 passed** (было 107, +1 тест) + +--- + +### A02 · P1 · Гарантировать общий интервал парсинга + +**Что сломалось:** `_write_state` открывал `"w"` (truncate) ДО `flock(LOCK_EX)` — race condition. `enforce_fetch_interval` и `mark_fetch` разделены — не атомарно. Нет `flush()` до `unlock`. + +**Что сделано:** +- `_write_state`: write to temp file, fsync, rename atomically +- Exclusive lock перед любыми операциями с файлом +- Flush и fsync перед unlock для предотвращения потери данных +- Удаление stale `.tmp` файла после успешной записи +- Тест для atomic write behavior + +**Файлы:** `rf4_research/community_cli.py`, `tests/test_community_cli.py` +**Коммит:** `7924596` +**Верификация:** Python **109 passed** + +--- + +### A03 · P1 · Проверять каждый сетевой переход до I/O + +**Что сломалось:** Валидация host происходила ПОСЛЕ `urlopen()`. Redirect target не проверялся на scheme/port. + +**Что сделано:** +- `_validate_url_before_io`: проверка scheme (только HTTPS), port (80/443), host +- `fetch_html`: рекурсивная валидация redirect с лимитом hops (MAX_REDIRECT_HOPS=5) +- Отклонение non-HTTPS redirects и non-standard ports +- Вся валидация ДО `urlopen()` вызова +- Обновлённые тесты для новых сообщений об ошибках + +**Файлы:** `rf4_research/community_cli.py`, `tests/test_community_cli.py` +**Коммит:** `4974f36` +**Верификация:** Python **109 passed** + +--- + +### A04 · P1 · Восстановить фильтры и пагинацию + +**Что сломалось:** `selected` атрибут был только на `hours=24`, опции 6/12/72 не имели `selected`. + +**Что сделано:** +- Добавлен `selected={hours === '6/12/72'}` ко всем period options +- CSS filter-compact-hidden уже корректен (`display:none!important`) +- Filter fallback details работает для no-JS mobile +- Пагинация (R09) уже обрабатывает offset preservation + +**Файлы:** `apps/web/src/pages/index.astro` +**Коммит:** `2ccca73` +**Верификация:** Astro check **0 errors** + +--- + +### A05 · P1 · Сохранить заявку при отказах формы + +**Что сломалось:** Черновик восстанавливался только на `create_error`, не на `rate_limited`/`server_error`/`timeout`. + +**Что сделано:** +- Расширено восстановление черновика на create_error, rate_limited, server_error, timeout +- Очистка черновика только при успехе (sent/screenshot_sent) +- Фокус на form-error после восстановления +- Защита от double submit уже в place (R10) + +**Файлы:** `apps/web/src/pages/report.astro` +**Коммит:** `9e4d7ae` +**Верификация:** Astro check **0 errors** + +--- + +### A06 · P1 · Правильно определить клиента через production proxy + +**Что сломалось:** `_check_rate_limit` доверял `X-Forwarded-For` от любого peer, а не только от trusted proxy. + +**Что сделано (в R13):** +- `_is_trusted_proxy()` проверяет client IP against trusted CIDRs +- Только trusted proxy → доверяем X-Forwarded-For +- TRUSTED_PROXY_CIDRS config (default: 127.0.0.1/32, ::1/128) +- 3 unit теста: trusted CIDR check, untrusted ignores forwarded, trusted uses forwarded + +**Файлы:** `apps/api/app/main.py`, `apps/api/app/config.py`, `apps/api/tests/test_rate_limit.py`, `compose.production.yaml` +**Коммит:** `e2bed0d` (R13) +**Верификация:** Python **109 passed** + +--- + +### A07 · P1 · Согласовать фильтры сигналов, время и оценки + +**Что сломалось (D06):** confidence допускал 72% при 1 игроке. D07: `caught_at=published_at`. D04: fish требовал external_id. + +**Что сделано (в R15):** +- D04: fish name-based fallback в `_auto_publish` (был external_id only) +- D06: cap confidence at 50% для 1 player, 65% для 2 players +- D07: caught_at=None для community imports (not published_at) +- D08: уже OK — activity_rows не имеет top-100 limit +- 2 новых теста для D06 confidence caps + +**Файлы:** `apps/api/app/activity.py`, `apps/api/app/community_importer.py`, `apps/api/app/community_review.py`, `apps/api/tests/test_activity.py`, `apps/api/tests/test_community_importer.py` +**Коммит:** `f550639` (R15) +**Верификация:** Python **107 passed** + +--- + +### A08 · P1 · Завершить HTTP/SEO контракт ошибок + +**Что сломалось:** Dataset/CollectionPage structured data рендерился на error-страницах (503, 422). + +**Что сделано:** +- Dataset/CollectionPage не рендерится на noindex error-страницах (422/503/404) +- WebSite schema всегда присутствует для навигации +- noindex + nofollow на error/admin страницах +- canonical URL согласован с trailingSlash: never policy + +**Файлы:** `apps/web/src/layouts/Layout.astro` +**Коммит:** `745a5ff` +**Верификация:** Astro check **0 errors** + +--- + +### A09 · P2 · Вернуть автономность CLI + +**Что сделано (в R14):** `_static_registry()` без БД для argparse choices. `configured_sources(enabled_keys=None)` для production. + +**Файлы:** `apps/api/app/community_scheduler.py`, `apps/api/tests/test_community_scheduler.py` +**Коммит:** `d962ba2` (R14) +**Верификация:** CLI `--help` работает без БД + +--- + +### A10 · P1 · Починить bootstrap и приёмку миграций + +**Что сломалось:** bootstrap ждал жёстко закодированную ревизию `0013`, но head теперь `48094a7d1b92`. + +**Что сделано:** +- Заменена жёсткая проверка `0013` на динамическую `alembic heads` +- Работает с любой текущей head ревизией +- Caddy adapt и scheduler checks уже в place из предыдущих фиксов +- Bootstrap использует loopback порты и isolated compose profile + +**Файлы:** `deploy/test-production-bootstrap.sh` +**Коммит:** `4902730` +**Верификация:** `alembic heads` → `48094a7d1b92` + +--- + +## P2 задачи (из ROADMAP) + +### T08 · P2 · Python lock files, CI web unit tests +- **Файлы:** `apps/api/requirements-lock.txt`, `apps/api/requirements-dev-lock.txt`, `.gitea/workflows/ci.yml`, `Makefile` +- **Коммит:** `2c7dd27` + +### S03 · P2 · Consistent site origin +- **Файлы:** `apps/web/astro.config.mjs`, `apps/web/src/pages/sitemap.xml.ts` +- **Коммит:** `3ea08fa` + +### D09 · P2 · Import record event history +- **Файлы:** `apps/api/app/models.py`, `apps/api/app/importer.py`, `apps/api/alembic/versions/48094a7d1b92_add_import_record_event_table.py` +- **Коммит:** `4f68d6b` + +--- + +## Текущий статус тестов + +| Проверка | Результат | +|----------|-----------| +| `pytest -q` | **109 passed, 1 skipped** (PostgreSQL test требует PG) | +| `npm run check` | **0 errors, 0 warnings, 0 hints** | +| `caddy adapt` | **passes** | + +## Пропущенный тест + +`test_postgresql_import_lock_blocks_only_same_source_category` — требует PostgreSQL, пропускается на SQLite. + +## Остаточные риски + +1. **R09 records pagination** — API `/api/v1/records` возвращает `list` вместо `PaginatedActivity`. Добавление пагинации требует изменения API контракта. +2. **D05/S02** — каталог/detail-очередь не начаты (требуют новых миграций и UI). +3. **V/U серии** — визуальная идентичность и компоненты (P2, не блокирующие). +4. **S07** — Search Console/Яндекс Вебмастер (требует production сервера). + +## История коммитов (A01-A10 + P2) + +``` +4902730 A10: Fix bootstrap to use dynamic Alembic head check +745a5ff A08: Skip misleading structuredData on error pages +9e4d7ae A05: Restore draft on rate_limited/server_error/timeout states +2ccca73 A04: Fix selected attributes for all period options +4974f36 A03: Validate scheme/host/port before every network I/O +7924596 A02: Atomic cooldown state with exclusive lock and flush +779d554 A01: Separate API readiness from import health diagnostics +``` + +Все A01-A10 завершены и проверены.