feat: extend admin operations and media review

This commit is contained in:
ik
2026-09-16 19:50:01 +07:00
parent 4c75db1f74
commit 9bcb019d3a
13 changed files with 1063 additions and 28 deletions
+389
View File
@@ -0,0 +1,389 @@
# Полный аудит проекта RF4 Spotter — 10 сентября 2026
База: `13e04e6`. Проверены: архитектура, backend, frontend, security, performance, testing, deployment, documentation.
---
## 1. Архитектура и высокоуровневый обзор
### Стек
| Слой | Технология | Статус |
|------|-----------|--------|
| **Proxy** | Caddy 2.10 | Production-ready, TLS завершение, Basic Auth на admin |
| **Frontend** | Astro 7 SSR (Node) | Сборка 0 errors, адаптивный дизайн |
| **Backend** | FastAPI + SQLAlchemy 2 + PostgreSQL 17 | 130 тестов, 1 skipped |
| **Хранилище** | PostgreSQL 17 + MinIO/S3 | Volumes, backup/restore |
| **CI** | Gitea Actions | Python, Astro, Compose E2E, pip-audit |
| **Scheduler** | Official import + Community scheduler | Опциональный профиль |
### Оценка архитектуры: ✅ Хорошо (8/10)
- Чёткое разделение: Caddy → Astro SSR → FastAPI → Postgres/MinIO
- Нет точки отказа в виде единого контейнера
- Profiles в docker-compose для scheduler/importer
- Production compose отдельно от dev
---
## 2. Backend (FastAPI) — детальный разбор
### 2.1 `main.py` (~1100 строк) — 🔴 КРИТИЧЕСКАЯ ПРОБЛЕМА
**Проблемы:**
- Все endpoints в одном файле — нарушение SRP
- `create_catch_report` — 50+ строк бизнес-логики inline
- `_check_rate_limit` и `_is_trusted_proxy` — утилиты должны быть в отдельном модуле
- `_admin`, `_spot_or_404`, `_report_source` — общие функции смешаны с endpoints
- Нет APIRouter — всё на `app.get/post`
**Рекомендация:** Разделить на `routes/` с `APIRouter`:
```
apps/api/app/routes/
├── activity.py # /api/v1/activity, /spots/*
├── submissions.py # catch-reports, screenshots
├── admin.py # /admin/*
├── community.py # observations, source-status
├── records.py # /records, /imports
└── catalog.py # fishes, waterbodies, baits
```
### 2.2 `activity.py` — ⚠️ Есть риски при scale
**Плюсы:**
- Детерминированная формула, покрыта тестами
- Cap confidence для 1-2 игроков (A07)
- Exponential decay freshness (18h half-life)
**Риски:**
- Нет индекса на `(spot_id, fish_id)``groups` агрегирует в памяти
- При 10k+ approved reports за 72h — полный load + sort в памяти
- `total = len(rows)` — нет `COUNT` запроса, полная выборка для pagination
**Рекомендация:**
1. Добавить composite index: `ix_catch_report_spot_fish_approved`
2. Для activity endpoint — materialized view или pre-aggregation table
3. Для total — отдельный `COUNT` query вместо `len(rows)`
### 2.3 `models.py` — ✅ Хорошо структурирован
**Плюсы:**
- Чёткие enum для SourceType, ModerationStatus, ImportStatus
- Unique constraints на slug, external_id
- JSON поля для raw_payload, changes, provenance
- Foreign keys с relationships
**Проблемы:**
- `source_external_id` unique=True — конфликт при повторном импорте разных sources с одинаковым ID
- `SubmissionAttempt` — нет cleanup cron, растёт бесконечно (хотя есть cleanup в `_check_rate_limit`)
- `ImportRecordEvent` — нет индекса на `(catch_report_id, created_at)` для history query
### 2.4 `importer.py` — ✅ Хорошо, но есть edge cases
**Плюсы:**
- Advisory lock для cross-process mutex
- ETag/Last-Modified conditional requests
- Idempotent по external_id SHA-256
- A12: meaningful changes/provenance в ImportRecordEvent
**Риски:**
- `html: str | None = None` — тестовый параметр в production signature
- `_unique_slug` — N+1 query loop при коллизиях
- Нет retry policy для HTTP 5xx с exponential backoff
### 2.5 `retention.py` — ✅ Хорошо
**Плюсы:**
- Dry-run mode по умолчанию
- Configurable policy через RetentionPolicy
- Handles screenshots, payloads, moderation events
- Автоматический rejection pending reports по TTL
**Проблемы:**
- `delete_object` callback — должен быть injected, не passed per-call
- Нет bulk delete — loop по reports
### 2.6 `scheduler.py` vs `community_scheduler.py` — ⚠️ Дублирование
Оба используют pattern:
```python
while True:
try:
run_due_import()
except Exception:
logger.exception(...)
time.sleep(interval)
```
**Рекомендация:** Общий base class или use APScheduler/celery-beat
---
## 3. Frontend (Astro) — детальный разбор
### 3.1 `index.astro` — ⚠️ Сложная логика в template
**Проблемы:**
- 50+ строк бизнес-логики в `.astro` файле
- Нет error boundary — catch all делает 503
- `offset + items.length < totalItems` — работает, но fragile при partial last page
- Нет optimistic UI / loading state
**Рекомендация:**
1. Вынести fetch logic в `src/lib/fetchers.ts`
2. Добавить `isLoading` state и skeleton UI
3. Error boundary per-section (filters, activity, signals)
### 3.2 `Layout.astro` — ✅ Хорошо
**Плюсы:**
- A08: errorPage prop для skip structuredData
- Canonical, OG, Twitter Card, JSON-LD
- robots/noindex для admin/error pages
- Skip link для accessibility
**Минусы:**
- 12 CSS imports — можно aggregate в `global.css`
- `replaceAll("<", "\\u003c")` — hack для JSON escaping
### 3.3 `global.css` (~1200 строк) — ⚠️ Все стили вместе
**Проблемы:**
- Все компоненты в одном файле — сложно navigate
- Нет CSS modules или scoped styles
**Рекомендация:**
- Разделить на `components/`, `pages/`, `utilities/`
- Astro scoped styles для компонентов
---
## 4. Security — ✅ В целом хорошо, есть улучшения
### ✅ Реализовано
| Мера | Статус |
|------|--------|
| Rate limiting (PostgreSQL advisory lock) | ✅ |
| Idempotency-Key header | ✅ (A05) |
| HMAC client_hash вместо IP | ✅ |
| Screenshot validation (MIME, size, EXIF removal) | ✅ |
| Admin Bearer token + Caddy Basic Auth | ✅ |
| Security headers (HSTS, X-Frame-Deny, etc.) | ✅ |
| Production settings validation | ✅ |
| pip-audit в CI | ✅ |
### ⚠️ Улучшения
| Проблема | Приоритет |
|----------|----------|
| **CORS origins** — dev defaults `localhost:4321`, production требует HTTPS | Medium |
| **ADMIN_TOKEN** — default `change-me-in-production` в compose.yaml | High (docs) |
| **X-Content-Type-Options** — только на admin/catch-reports, не на всех | Low |
| **Content-Security-Policy** — отсутствует | Medium |
| **Rate limit** — 5 requests per 10 min, нет per-endpoint limits | Low |
| **Screenshot upload** — нет CSRF protection (POST без token) | Medium |
---
## 5. Performance — ⚠️ Есть узкие места
### 5.1 Database
**Проблемы:**
- `activity` endpoint: full table scan + in-memory sort
- `spot_detail`: `count_since` — Python loop по reports
- `public_spot_pages`: 3-way JOIN + DISTINCT + offset pagination
**Рекомендации — SQL индексы:**
```sql
-- Добавить индексы
CREATE INDEX ix_catch_report_spot_fish_approved
ON catch_report(spot_id, fish_id, moderation_status, reported_at);
CREATE INDEX ix_catch_report_waterbody_approved_reported
ON catch_report(waterbody_id, moderation_status, reported_at);
CREATE INDEX ix_catch_report_official_records
ON catch_report(source_type, caught_at, weight_g)
WHERE source_type = 'official_record';
```
### 5.2 Caching
**Текущее:**
- In-memory cache: 20s TTL, 128 keys, process-local
- `public_cache.invalidate()` на moderation/publish
**Проблемы:**
- Multi-process: каждый API worker имеет свой cache
- Нет cache warming на restart
- Community observations не кэшируются
**Рекомендация:** Redis или shared memory (SHM) для multi-process
### 5.3 Frontend
**Плюсы:**
- Astro SSR — no client JS for initial render
- `fetchpriority="high"` для hero image
- Reduced motion support
**Улучшения:**
- Нет lazy loading для нижеfold images
- Нет prefetch для `/spots/[id]`
- SignalFeed — нет intersection observer для "load more"
---
## 6. Testing — ✅ 130 passed, 1 skipped
### ✅ Хорошее покрытие
- API endpoints (test_api.py)
- Import idempotency (test_importer.py)
- Community CLI state/validation (test_community_cli.py)
- Readiness aggregation (test_readiness.py)
- Rate limit with Docker chain (test_rate_limit.py)
### ⚠️ Пропущенные сценарии
| Тест | Приоритет |
|------|----------|
| **Retention cleanup** — нет интеграционного теста | High |
| **Screenshot validation** — edge cases (PNG vs JPEG magic bytes) | Medium |
| **Community scheduler retry backoff** — 30min → 24h | Medium |
| **Activity calculation** — edge cases (0 players, all same player) | Low |
| **Bootstrap E2E** — test-production-bootstrap.sh не в CI | Medium |
| **Astro accessibility** — audit:axe есть, но нет automated checks | Low |
---
## 7. Deployment & Operations
### ✅ Хорошее
- Compose production с healthchecks
- Backup/restore scripts
- Monitoring docs (disk, TLS, readiness)
- Data retention policy
- Preflight checks before deploy
### ⚠️ Улучшения
| Проблема | Решение |
|----------|---------|
| **Нет blue-green/canary** | Добавить rolling update в deploy script |
| **Database migration rollback** | Нет test-rollback в bootstrap |
| **Log rotation** — Docker json-file, 10MB × 5 | OK, но нет centralized logging |
| **No distributed tracing** | Добавить OpenTelemetry для request_id propagation |
| **No metrics export** | Нет Prometheus metrics (request count, latency, error rate) |
---
## 8. Documentation
### ✅ Хорошее
- README.md — comprehensive (150+ строк)
- docs/ — recovery plans, audit reports, roadmap
- deploy/README.md — production steps
- data-policy.md, retention, monitoring
### ⚠️ Улучшения
| Проблема | Решение |
|----------|---------|
| **API documentation** — FastAPI auto-docs, но нет OpenAPI spec file | Добавить `openapi.json` в repo |
| **Data model ER diagram** | Добавить визуальную схему |
| **Architecture decision records (ADR)** | Для key decisions (Astro vs Next, in-memory cache, etc.) |
| **Runbook for incidents** | Что делать при Postgres full, MinIO down, etc. |
---
## 9. Код-стиль и maintainability
### ✅ Хорошее
- Type hints everywhere
- `from __future__ import annotations`
- Pydantic v2 models
- Alembic migrations
### ⚠️ Улучшения
| Проблема | Пример |
|----------|--------|
| **Long lines** | `create_catch_report` — 70+ char lines |
| **Magic numbers** | `55 * min(1, weighted / 12)` — вынести константы |
| **Inline SQL** | `text("SELECT pg_try_advisory_lock(:key)")` — вынести в constants |
| **Duplicate URL patterns** | `"/api/v1/admin/"` repeated in middleware and endpoints |
---
## 10. Приоритизированный список улучшений
### 🔴 High Priority (блокирующие/рисковые)
1. **Разделить main.py на routers** — SRP, testability, reviewability
2. **Добавить database indexes**`spot_id + fish_id + moderation_status`, `source_type + official_records`
3. **Retention integration test** — verify cleanup works end-to-end
4. **Screenshot CSRF protection** — form submission без CSRF token
5. **Bootstrap E2E в CI**`test-production-bootstrap.sh` должен run на push
### 🟡 Medium Priority (улучшения)
6. **Shared cache (Redis)** — вместо in-memory для multi-process
7. **Content-Security-Policy header** — добавить base-uri, script-src
8. **Error boundaries в Astro** — per-section error handling
9. **Materialized view для activity** — pre-aggregation вместо in-memory sort
10. **OpenAPI spec export**`openapi.json` в repo + swagger UI
11. **Metrics export (Prometheus)** — request latency, error rate, queue depth
12. **Screenshot validation test** — magic bytes, MIME mismatch
### 🟢 Low Priority (косметика/технический долг)
13. **CSS modularization** — split global.css into components
14. **Extract constants** — 55, 25, 20, 12, 6, 3 → named constants
15. **Scheduler base class** — DRY для official + community scheduler
16. **Prefetch links**`/spots/[id]` prefetch на hover
17. **ADR documentation** — record architecture decisions
18. **Incident runbook** — playbooks для common failures
---
## Итоговая оценка
| Категория | Оценка | Комментарий |
|-----------|--------|-------------|
| **Архитектура** | 8/10 | Чистое разделение, но main.py — god object |
| **Безопасность** | 7.5/10 | Хорошие основы, нужно CSP + CSRF |
| **Производительность** | 6.5/10 | Indexes + caching critical для scale |
| **Тестирование** | 8/10 | 130 tests, но пропущены retention + bootstrap |
| **Deploy/Ops** | 7.5/10 | Good compose, no metrics/tracing |
| **Документация** | 8.5/10 | Comprehensive README, missing runbook |
| **Code Quality** | 7.5/10 | Type hints, но long files + magic numbers |
**Общий балл: 7.6/10** — крепкий проект с хорошими основами, требует refactor main.py и database indexes для production scale.
---
## История ревизий
| Дата | Автор | Изменения |
|------|-------|----------|
| 2026-09-10 | AI | Полный аудит: архитектура, backend, frontend, security, performance, testing, deploy, docs |
| 2026-09-08 | (предыдущий) | Базовый аудит UI/UX, T01/T02 fixes |
---
## Что делать дальше (рекомендации)
### Немедленно (до продакшена)
1. Разделить main.py на routers (минимум 3 файла)
2. Создать и применить SQL индексы
3. Добавить retention integration test
4. Добавить CSRF token для screenshot upload
5. Добавить `test-production-bootstrap.sh` в CI workflow
### В течение спринта
6. Внедрить Redis для shared caching
7. Добавить CSP headers
8. Вынести fetch logic из index.astro в fetchers.ts
9. Добавить OpenAPI spec export
10. Написать incident runbook
### По возможности
11. Materialized view для activity
12. Prometheus metrics
13. CSS modularization
14. ADR documentation
15. Scheduler base class
+47 -6
View File
@@ -2,15 +2,15 @@
Этот файл — единственный актуальный список задач. Завершённые аудиты сохранены как история в [PROJECT_AUDIT_2026-09-08.md](PROJECT_AUDIT_2026-09-08.md), [REGRESSION_AUDIT_2026-09-09.md](REGRESSION_AUDIT_2026-09-09.md) и [RECOVERY_PLAN_2026-09-10.md](RECOVERY_PLAN_2026-09-10.md); их старые чекбоксы не являются текущей очередью.
Последняя сверка: **14 сентября 2026**.
Последняя сверка: **15 сентября 2026**.
Подтверждено:
- [x] пакет восстановления A01–A13 завершён; итог и доказательства собраны в [RECOVERY_FIXES_REPORT.md](RECOVERY_FIXES_REPORT.md);
- [x] полный Python suite: **157 passed, 1 skipped**; skip относится к интеграционной проверке PostgreSQL и покрывается Docker-приёмкой;
- [x] полный Python suite: **178 passed, 1 skipped**; skip относится к интеграционной проверке PostgreSQL и покрывается Docker-приёмкой;
- [x] Astro check: 40 файлов, **0 errors / 0 warnings / 0 hints**; production build проходит;
- [x] API после миграции healthy; `apps/api/tests/test_api.py`: **20 passed**;
- [x] граф Alembic линеен и имеет единственную голову `0016`; CI применяет её на чистой PostgreSQL, полный production bootstrap запускается отдельным еженедельным drill;
- [x] граф Alembic линеен и имеет единственную голову `0018`; CI применяет её на чистой PostgreSQL, полный production bootstrap запускается отдельным еженедельным drill;
- [x] изолированный production bootstrap проходит Caddy adapt, scheduler validation и Playwright-сценарий отправки/модерации без обращения к внешним источникам;
- [x] Astro + FastAPI + PostgreSQL остаются целевым стеком; Next.js и Vinext не используются.
@@ -38,14 +38,43 @@
- [x] **B16 · Самодостаточное хранение media dataset.** Все управляемые оригиналы из `data/media/files/` версионируются в Git вместе с manifest; локальный клон содержит полный исследовательский набор. Пользовательские скриншоты остаются в MinIO/S3, а наличие файла в Git не означает разрешение на публикацию без статуса `approved`.
- [ ] **B17 · Полный каталог рыб RF4DB — финальная загрузка.** Разрешённый RF4DB дал все 252 подписанных кандидата; 227 совпадений с RF4MAP сохранены как provenance-only `duplicate`, 15 уникальных файлов скачаны, а 10 URL (9 уникальных названий) остаются в очереди после корректно остановленного TLS-сбоя. Все поддомены RF4DB объединены одним 30-минутным cooldown. Завершить следующим разрешённым batch-окном и отдельно утвердить новые файлы.
- [ ] **B18 · Каталог водоёмов RF4DB.** После cooldown проиндексировать 19 русских карточек водоёмов, проверить полноразмерные карты и поставить только уникальные изображения в media queue.
- [ ] **B19 · Каталог снастей RF4DB.** После водоёмов проиндексировать категории gear, построить crosswalk по типу, бренду, семейству и названию; не считать каталог полным до получения проверяемого общего счётчика.
- [ ] **B19 · Каталог снастей RF4DB.** Подготовительный media-контур; полный справочник, crosswalk, связи с уловами и публичные страницы выполняются по пакету **G01G09** ниже. Не считать каталог полным до получения проверяемого общего счётчика по категориям.
- [x] **B20 · Аудит качества рыбных изображений.** Offline `media_cli --quality-report` проверяет фактические dimensions опубликованных файлов, отдельно считает неизбежный upscale в карточках и известные альтернативы. На 14.09: 227 из 243 опубликованных рыб имеют только 48×48 PNG RF4MAP и растягиваются до 180 px; 16 имеют 1024×1024 WebP. Для всех 227 низких файлов уже известны альтернативные URL RF4DB. Совпадение сущности больше нельзя считать достаточным основанием пропустить потенциально более качественный файл.
- [ ] **B21 · Очередь quality-upgrade.** Не менять текущие approved-файлы до готовности замены. Перевести 227 RF4DB `duplicate` в отдельное состояние `upgrade_queued`, сохранив `duplicate_of`, и загружать партиями до 40 в разрешённые 30-минутные окна. Ошибка останавливает домен по существующим правилам; публикация старой версии при этом не прерывается.
- [ ] **B22 · Сравнение вариантов разных источников.** После загрузки для каждой рыбы сравнить реальные dimensions, размер файла, MIME, прозрачность, aspect ratio и визуальное соответствие подписи. Минимальный технический порог для основной карточки — 256 px по меньшей стороне, предпочтительный — 512 px; маленький файл сохранять только как fallback. Источник RF4DB/RF4MAP/официальный RF4 не получает автоматического приоритета: выбирается лучший прошедший проверку файл.
- [ ] **B21 · Очередь quality-upgrade.** Текущие approved-файлы не меняются до готовности замены. 15.09 отдельная очередь создана для 226 прямых RF4DB-альтернатив с сохранением `duplicate_of`; первая партия из 40 файлов загружена без ошибок в `upgrade_stored`, ещё 186 замен и 10 кандидатов недостающих рыб ожидают следующих разрешённых 30-минутных окон. Один provenance-only `duplicate`, не связанный с низкоразрешённым published-файлом, намеренно не переведён. Ошибка останавливает домен по существующим правилам; публикация старой версии при этом не прерывается.
- [ ] **B22 · Сравнение вариантов разных источников.** Offline-команда `--compare-quality-upgrades` сопоставляет сохранённые кандидаты с опубликованными fallback по `duplicate_of` и проверяет dimensions, размер файла, MIME, прозрачность и aspect ratio. Первая партия: 40/40 кандидатов — прозрачные WebP 1024×1024, все прошли порог 256 px и сохранили пропорции; технических ошибок нет. До переключения остаётся визуально подтвердить соответствие подписи. Маленький файл сохраняется как fallback; источник RF4DB/RF4MAP/официальный RF4 не получает автоматического приоритета.
- [ ] **B23 · Безопасное продвижение и provenance.** Добавить связь `supersedes`/`replaced_by`, отдельное решение review и атомарное переключение публичного `entity_key`; сохранить обе исходные ссылки и возможность отката. На сайте показывать источник именно выбранного изображения, а в раскрываемом provenance — все проверенные варианты. Не удалять старый Git-файл в том же коммите, где включается новый.
- [ ] **B24 · Производные размеры.** После выбора оригиналов генерировать детерминированные WebP/AVIF thumbnails для каталога и отдельный крупный вариант для detail, фиксировать хэши производных в manifest и отдавать `srcset`. Это уберёт загрузку 1024×1024 на каждой маленькой карточке и исключит browser-upscale 48×48.
- [ ] **B25 · Визуальная приёмка media.** Собирать контактный лист «старое / кандидат / выбранное» с названием и источником; вручную проверить минимум все замены и репрезентативные desktop/mobile страницы в light/dark. Gate: нет битых файлов, искажённых пропорций, ложных соответствий, обрезанного объекта и изображений ниже 256 px без явной пометки «низкое разрешение».
### Каталог водоёмов, карты и координаты
- [x] **W01 · Canonical-каталог RF4DB.** 16.09 через браузерный контекст получен и проверен индекс `/ru/maps`: 19/19 карточек, source slug/ID, русское название, уровень открытия, число видов рыб и URL изображения сохранены в [`data/waterbodies/rf4db-catalog-2026-09-16.json`](../data/waterbodies/rf4db-catalog-2026-09-16.json). Добавлена команда `python -m app.cli import-waterbody-catalog --input ...` для применения снимка в БД. Изображения остаются кандидатами без автоматической роли `map` или `cover`; detail-данные выполняются отдельно по W02.
- [ ] **W02 · Карточки водоёмов RF4DB.** Строгий fixture-based parser `rf4db-waterbody` расширен под реальный DOM `/fishes/` и `/positions/`, regression-тесты, безопасный `update_waterbody_detail` и атомарный batch `update_waterbody_details`/`import-waterbody-details` готовы; первая detail-карточка (`level_000_home`) сохранена браузером как snapshot. Осталось последовательно разобрать 18 detail-страниц с общим 30-минутным cooldown домена.
- [ ] **W03 · Модель и provenance.** В модель `Waterbody`, API-каталог и миграции `0017`/`0019`/`0020` добавлены nullable-поля provenance, счётчик видов и detail-факты; идемпотентные upsert-функции обновляют только подтверждённые source identity и не удаляют исчезнувшие строки. Осталось применить их к проверенному canonical-каталогу и отдельно разделить игровой и редакционный тексты при подключении detail-данных.
- [ ] **W04 · Классификация изображений.** Добавлены допустимые роли `waterbody_cover`, `waterbody_map`, `waterbody_depth_map`, `waterbody_screenshot` и проверка их назначения только через review для canonical waterbody. Кандидаты по-прежнему не получают роль автоматически. Осталось наполнить очередь detail-изображениями и провести contact-sheet review с проверкой dimensions, MIME, SHA-256, соответствия названию и источника.
- [ ] **W05 · Crosswalk источников.** Добавлен offline-конструктор консервативных предложений: нормализуются только точные имена/алиасы, неоднозначные и unmatched строки не получают canonical key; отсутствие ID выдаётся лишь диагностикой и не считается удалением. Осталось подать реальные RF4DB/RF4MAP/RF4 Posts identities и вручную подтвердить результаты, включая три ранее отмеченных отсутствующих RF4MAP объекта.
- [ ] **W06 · Координаты и точность.** В `ExternalObservation`, staging, provenance опубликованного улова и публичных activity/spot-ответах добавлены `coordinate_raw`, `coordinate_precision = exact | approximate | area | missing` и список источников; RF4DB/RF4-STAT/RF4MAP/RF4 Posts parsers теперь протягивают исходную строку, включая строки без доступных числовых координат. Карточка точки показывает точность рядом с координатами. Осталось провести browser QA.
- [ ] **W07 · Публичный API и страницы.** API и detail-страница теперь выводят подтверждённые detail-факты водоёма: описание, уровень, количество видов, алиасы, число ссылок на точки и отдельный счётчик изображений-кандидатов; источники и непроверенные media не смешиваются. Осталось подключить только проверенные waterbody media roles и завершить browser QA, включая различение карты, заставки и абстрактного отпечатка.
- [ ] **W08 · Приёмка и эксплуатация.** Добавить fixture-based parser tests, offline catalog/media audit, проверку 19 canonical entities, отсутствие битых файлов и browser QA desktop/mobile. Сетевые тесты не выполнять; регулярный импорт оставить opt-in и под общим cooldown/backoff.
### Каталог снастей, наживок и прочей оснастки
Этот пакет повторяет жизненный цикл W01–W08, но не смешивает разные уровни
описания. Наживка/приманка — предмет, который указан в улове; снасть —
компонент комплекта (удилище, катушка, леска, крючок и т. п.); оснастка —
собранная схема или монтаж (например, донная, поплавочная, method). Число
найденных изображений приманок не считается размером полного каталога.
- [ ] **G01 · Canonical-каталог RF4DB.** Получить разрешённый индекс категорий gear и проверить полный набор доступных страниц по типам: `bait`, `lure`, `rod`, `reel`, `line`, `hook`, `rig`, `float`, `sinker`, `other`. Сохранить исходный slug/ID, русское название, категорию, подкатегорию, бренд, семейство, игровые ограничения/уровень и source URL. Отсутствие общего счётчика или закрытая категория должны оставаться явно `unknown`, а не превращаться в оценку полноты.
- [ ] **G02 · Карточки предметов и оснасток.** Подготовить строгие fixture-based parsers для списка и detail-страницы: характеристики, варианты, совместимость, изображения, связанные типы монтажа и исходные значения. Парсер должен различать отсутствующее поле, «не применимо» и фактическое нулевое значение; при неполном или изменившемся ответе сохранять предыдущие подтверждённые данные. Detail-запросы выполнять только для выбранных карточек и с общим 30-минутным cooldown домена.
- [ ] **G03 · Модель и provenance.** Расширить `bait` до канонического `tackle_item` либо выполнить безопасную миграцию с обратной совместимостью API: `kind`, `category`, `subcategory`, `brand`, `family`, `source_system`, `source_external_id`, `source_url`, `source_checked_at`, `raw_payload`. Отдельно моделировать `rig`/монтаж и его компоненты; не хранить удилище, катушку и монтаж в одном свободном `rig_type`. Для каждой характеристики сохранить источник и статус проверки.
- [ ] **G04 · Crosswalk и нормализация.** Построить offline-crosswalk между RF4DB, официальными рекордами, RF4MAP, RF4 Posts и локальным справочником. Нормализовать регистр, пробелы, дефисы, единицы и локализацию; предлагать совпадение только при точном имени/алиасе плюс совместимой категории. Неоднозначные, брендовые варианты и unmatched-строки отправлять на review без автоматического canonical key; исходное значение всегда сохранять.
- [ ] **G05 · Связи с уловами и источниками.** Протянуть канонические предметы и монтажи через community import, официальные записи и форму улова, сохранив `raw_payload` и список missing fields. Поддержать несколько предметов в одном комплекте, порядок/роль компонента и источник каждой связи; старый `bait_id` и текстовые значения не терять при миграции. Публикация полного наблюдения по-прежнему требует подтверждённых соответствий, а не простого совпадения строки.
- [ ] **G06 · API и публичный каталог.** Добавить пагинированные каталоги и detail endpoints с фильтрами по категории, бренду, семейству и уровню, а также безопасные ссылки из улова/точки на использованную приманку, снасть и монтаж. Показывать только подтверждённые характеристики, источник, свежесть и неполноту; не выдавать рейтинг эффективности, если его нельзя объяснить числом наблюдений, игроками, периодом и качеством источников.
- [ ] **G07 · Аналитика сочетаний и рекомендации.** После появления достаточных данных считать отдельно «водоём + рыба + предмет», «точка + рыба + предмет» и «способ ловли + монтаж». Зафиксировать минимальный объём выборки, защиту от одного игрока/дубликатов и decay по свежести; разделить факт использования, частоту и рекомендацию. Пустая или малая выборка должна показывать «данных мало», а не советовать конкретную снасть.
- [ ] **G08 · Медиа и качество.** Разнести media roles для `tackle_item`, `bait`, `rig` и общего reference; связать варианты через `entity_key`, `duplicate_of`, `supersedes`/`replaced_by`. Проверять dimensions, MIME, SHA-256, прозрачность, aspect ratio, подпись и категорию; не переключать approved-файл автоматически, не считать userguide-скриншот карточкой предмета и не публиковать media-кандидатов без review и разрешённого provenance.
- [ ] **G09 · Приёмка и эксплуатация.** Добавить fixture/regression tests, offline catalog/crosswalk/media audits, проверку идемпотентности и сохранения старых данных при сбое, API/UI acceptance для пустых, неоднозначных и многокомпонентных комплектов, browser QA desktop/mobile и query-plan gate для фильтров/сочетаний. Сетевые тесты не выполнять; импорт оставить opt-in, последовательным и под общим cooldown/backoff. Закрывать пакет только после проверяемого счётчика по каждой категории либо явной фиксации `unknown`.
### Тёмная тема
Реализовывать последовательно: сначала семантическая палитра и системный режим, затем ручное управление и полировка компонентов. Тёмная тема должна сохранять полевую эстетику RF4 Spotter, а не быть механической инверсией светлой.
@@ -91,6 +120,18 @@
- [x] **M05 · Защита от параллельных решений.** Обе очереди отдают `moderation_version`; mapping/publish/reject/approve/delete требуют увиденную версию и повторно сверяют её под row lock. Успешное решение атомарно увеличивает version, а устаревшая вкладка получает понятный `409` и автоматически перезагружает очередь. Схема обновляется линейной миграцией `0015`.
- [ ] **M06 · Персональные роли — после пилота.** Если модераторов станет больше одного, заменить общий токен индивидуальными аккаунтами, короткими сессиями, отзывом доступа и ролями; писать идентификатор оператора в аудит. Для одного владельца альфы не добавлять отдельный auth-сервис заранее.
### План доведения административной панели
Этот план фиксирует следующий рабочий контур поверх уже закрытых M01–M05. Текущий MVP функционален для одного владельца альфы, но ниже перечислены эксплуатационные пробелы, найденные аудитом, и критерии их закрытия.
- [ ] **A01 · Защита маршрутов и границы сессии.** Покрыть точный `/admin` и `/admin/*` единым Caddy Basic Auth, выставлять `noindex` и `no-store` для всех административных ответов, проверить отсутствие обхода через API и корректные `401/429`. Критерий: автоматический proxy-smoke для `/admin`, страниц и `/api/v1/admin/*` с отсутствующим, неверным и валидным доступом.
- [ ] **A02 · Единая auth/error UX.** Привести dashboard, moderation и external sources к одинаковому поведению при `401`, `409`, `429`, `5xx`, loading/empty-состояниях: понятное сообщение, блокировка повторной отправки, возврат к входу только при истёкшей авторизации. Критерий: regression-тесты на каждый ответ и сохранение введённой причины.
- [ ] **A03 · Полный single-owner workflow.** Добавить пагинацию очереди уловов, кнопку запуска официального импорта и отображение результата/истории, безопасные статусы источников с возрастом данных, cooldown/backoff и ручное обновление очередей. Критерий: владелец может пройти путь «импорт → проверка → решение → история» без API/CLI; старые данные сохраняются при сбое импорта.
- [ ] **A04 · Контур медиа-проверки.** Сделать admin-экран для просмотра approved/upgrade_queued медиа, исходника, размеров, производных и provenance; добавить approve/rollback только через существующие безопасные состояния. Критерий: ни одна публичная замена не происходит без явного решения и проверяемого manifest-а.
- [ ] **A05 · Многопользовательский доступ.** После пилота заменить общий Bearer-токен персональными аккаунтами и короткими серверными сессиями с отзывом, ролями read-only/moderator/importer/owner, operator ID в аудите и журналом входов. Критерий: минимальные права реально ограничивают действия, logout/revoke инвалидируют сессию на сервере.
- [ ] **A06 · Browser/accessibility acceptance.** Проверить `/admin`, moderation и external sources в 320/390/768/1280 px: клавиатура, focus order, screen reader labels, reduced motion, forced colors, темы, ошибки/пустые очереди и конфликт `409`. Критерий: Playwright + axe без блокирующих дефектов и ручная визуальная проверка.
- [ ] **A07 · Production gate.** Выполнить preflight с реальными секретами, проверить Caddy Basic + API Bearer, закрытые внутренние порты, backup/restore PostgreSQL и MinIO, readiness, no-store и внешний smoke после деплоя. Критерий: acceptance-runbook пройден, rollback и процедура отзыва доступа документированы.
## Готовность открытой альфы — требуется сервер или внешний сервис
- [ ] Купить/подготовить Linux-сервер и подтвердить его публичный IPv4/IPv6.