300 lines
45 KiB
Markdown
300 lines
45 KiB
Markdown
# RF4 Spotter
|
||
|
||
RF4 Spotter — неофициальный сервис свежих точек и статистики клёва для Russian Fishing 4. Он объединяет публичные рекорды и подтверждённые пользовательские уловы, показывает свежесть источников и рассчитывает объяснимые индексы активности и уверенности. Проект не взаимодействует с игровым клиентом и не является официальным продуктом RF4.
|
||
|
||
Код и оригинальные материалы проекта распространяются по [AGPL-3.0-only](LICENSE). Лицензия не распространяется на игровые материалы, данные сторонних источников и пользовательские загрузки. Правила обработки описаны в [политике данных](docs/data-policy.md).
|
||
|
||
## Статус разработки
|
||
|
||
**Проверка 11 сентября 2026 (`907ad53`): локальный контур готов к развёртыванию открытой альфы, внешний запуск ждёт сервер и его настройки.** Пакет восстановления A01–A13 закрыт. Python: **130 passed, 1 skipped**; Astro check/build и API-тесты проходят. Чистый production bootstrap подтвердил Caddy, scheduler validation, миграцию `20260910_recovery` и браузерный сценарий отправки/модерации. Реальные источники во время приёмки не опрашивались.
|
||
|
||
Актуальные следующие задачи находятся только в [ROADMAP](docs/ROADMAP.md). Старые планы и аудиты сохранены как история и больше не задают порядок работ. До внешнего запуска нужны сервер, DNS/TLS, production-секреты, публичные контакты, внешний backup и канал уведомлений.
|
||
|
||
Разрешённые интеграции и недостающие первичные подтверждения сведены в [реестр разрешений](docs/source-permissions.md). Перед открытой публикацией пустые поля реестра являются блокером конкретного источника, особенно для изображений.
|
||
|
||
Web Docker-образ устанавливает зависимости через `npm ci` по lock-файлу и удаляет devDependencies после сборки. Локальные `.env` исключены из web build context.
|
||
|
||
Очередь внешних наблюдений выбирает только ожидающие проверки записи на сервере и показывает их страницами по 50. Обработанные записи не скрывают более старые необработанные наблюдения.
|
||
|
||
Запросы Astro к API ограничены таймаутом 8 секунд на запрос. При недоступности API каталоги рыб/водоёмов возвращают HTTP 503 с Retry-After, вместо успешного ответа со страницей ошибки.
|
||
|
||
В очереди внешних наблюдений доступна кнопка «Подсказать соответствия»: она показывает ранее подтверждённые рыбу и водоём. Значения формы не меняются автоматически; сопоставление и публикация подтверждаются отдельно.
|
||
|
||
Повторный импорт изменённой опубликованной записи переводит её на ручную проверку и снимает прежний улов с активности (с учётом TTL кэша). После сопоставления и подтверждения обновляется тот же улов; дубликат не создаётся. Автоматическое обнаружение удалённых оригиналов пока не реализовано.
|
||
|
||
История исправлений аудита сохранена в [AUDIT_FIXES.md](docs/AUDIT_FIXES.md) и [RECOVERY_FIXES_REPORT.md](docs/RECOVERY_FIXES_REPORT.md). Production Compose включает community scheduler; страницы rules/privacy реализованы. Для запуска остаются сервер, DNS/TLS, секреты, внешний backup и контакты. Шкала 72 часов использует полную выборку по времени поступления; одинаковые поля разных источников больше не считаются доказательством одного события. Фоновая публикация обновляет кэш API в пределах TTL, не мгновенно.
|
||
|
||
Предыдущие аудиты и план восстановления доступны в `docs/` как исторические материалы. Их незакрытые на момент составления чекбоксы не являются текущим backlog; статусы сведены в [итоговый отчёт](docs/RECOVERY_FIXES_REPORT.md).
|
||
|
||
На ширинах 320, 390, 768 и 1280 px проверено отсутствие горизонтального переполнения основных страниц; исправлена desktop-компоновка фильтров. Наполненные карточки, длинные названия, клавиатура и zoom остаются постоянной частью визуальной приёмки каждого крупного UI-пакета.
|
||
|
||
Функциональный MVP и локальный production-контур готовятся к открытой альфе: официальный импорт, пользовательские заявки, модерация, объяснимый индекс, staging внешних источников, адаптивный Astro UI, миграции, резервное копирование, retention, мониторинг и security/accessibility-проверки реализованы. На всех страницах подключён компактный баннер открытой альфы со ссылками на статус, правила и отправку улова. В production Compose включён community scheduler; локально он запускается отдельным профилем. Публичный запуск блокируют покупка и настройка сервера, DNS/TLS, реальные секреты, внешний backup, канал уведомлений; публичный адрес обратной связи ещё не задан.
|
||
|
||
Главная деградирует по секциям: activity, community signals и справочники загружаются независимо. Частичный отказ сохраняет доступные данные и возвращает HTTP 200 с `X-RF4-Partial` и запретом кэширования; общий 503 возникает только при отказе всех частей.
|
||
|
||
RF4DB/RF4-STAT/RF4MAP/RF4 Posts сначала принимаются в изолированный staging. Полные записи с ранее подтверждёнными алиасами источника публикуются автоматически; новые соответствия и неполные записи остаются на ручной проверке. Admin API предлагает точные ранее подтверждённые алиасы отдельно от mapping-действия и запрещает молча переназначать alias другой сущности. Для разрешённых community-источников действует интервал не менее 30 минут на сайт, общий для всех его endpoint. Открытая альфа не использует продуктовый allowlist: интерфейс показывает весь корректно загруженный разрешённый каталог, сохраняя требования полноты и модерации.
|
||
|
||
На сайте у каждой записи отображается источник, а у агрегированной активности — все вошедшие в расчёт источники. Неполные community-наблюдения публикуются сразу в отдельной ленте «Полевые сигналы» с предупреждением и перечнем отсутствующих полей; до подтверждения полноты они не влияют на индекс клёва. Лента раскрывается серверной кнопкой «Показать ещё», сохраняет выбранные фильтры и ограничена 48 сигналами на страницу. Визуально объединяются только повторы одного ID источника; похожие записи разных площадок остаются самостоятельными наблюдениями. Sidebar лидера скрывается при единственном результате, чтобы не повторять ту же карточку.
|
||
|
||
Базовый SEO-контур готов для `rf4spotter.ru`: страницы имеют уникальные метаданные, canonical, Open Graph/Twitter Card, фирменное изображение 1200×630 и JSON-LD; доступны динамические `/robots.txt` и `/sitemap.xml`, административные и ошибочные страницы закрыты от индексации, добавлена собственная страница 404. Индексируемые каталоги рыб и водоёмов, detail-страницы и сочетания водоём + рыба строятся из актуального разрешённого справочника и включаются в sitemap. Подключены резкие favicon/app icons из SVG-мастера, отдельные полнофоновые maskable-иконки, web manifest и production-кэширование статических ресурсов. Карточки активности показывают единый паспорт данных: источники, свежесть, полноту и уровень доверия. На `/status` опубликована легенда цветов всех источников и статусов качества.
|
||
|
||
Публичные точки используют постоянные читаемые адреса вида `/spots/kuori-85x92`; старые UUID-адреса остаются совместимыми и перенаправляются на канонический URL. На странице точки координаты дополнительно показаны фирменным радаром, который не имитирует отсутствующую географию водоёма, а уловы за 72 часа — шкалой-леской с 12-часовым шагом. Каждый улов показывает источник, относительную свежесть и точное время UTC; время получения явно отделено от времени улова. Каталоги оформлены как полевой атлас: тёмный seal показывает объём справочника, карточки рыб используют смысловые SVG-силуэты, а каждый водоём — собственный детерминированный абстрактный отпечаток берега, волн, точки и индекса. На странице сочетания оба знака собираются в единую атласную эмблему, detail-иерархию связывает breadcrumb-леска с текстовыми узлами, а боковые переходы повторяют знаки связанных сущностей. Это не карта и не игровая география. Пустые состояния используют статичную CSS-иллюстрацию поплавка; смысловые анимации полностью учитывают системное ограничение движения.
|
||
|
||
Все пять community-парсеров подключены к отдельному scheduler-процессу. Попытка резервируется в PostgreSQL до HTTP-запроса, поэтому ошибки тоже расходуют cooldown. Блокировка и минимальный интервал 1800 секунд действуют на весь домен; endpoint одного сайта выбираются по самому давнему запуску и не голодают. Ручной production-запуск использует тот же журнал: `docker compose exec api python -m app.cli fetch-community rf4stat-fishing`. Локально scheduler включается профилем `docker compose --profile scheduler up -d`; detail-URL RF4MAP/RF4 Posts задаются переменными окружения.
|
||
|
||
Медиасборщик индексирует разрешённые изображения отдельно от публичного каталога: manifest хранит исходную страницу, URL, предполагаемый тип сущности и время обнаружения, а оригиналы сохраняются по SHA-256 без hotlink. Индексация страницы и загрузка каждого файла используют общий 30-минутный cooldown домена; непроверенный asset не публикуется автоматически. Локальный `media_cli --audit` без сетевых запросов проверяет хэши, файлы, MIME, размеры, approved-сопоставления и отсутствие бесхозных оригиналов.
|
||
|
||
`python -m rf4_research.media_cli --coverage` сравнивает manifest с датированным `data/media/catalog-baseline.json`: отдельно считает файлы, уникальные нормализованные подписи и кандидатов без подписи, поэтому дубли и общие учебные схемы не завышают покрытие. Сейчас не покрыты минимум 24 рыбы и все 19 водоёмов, а до ручного review не подтверждены 251 рыба и все 19 водоёмов. Общий target снастей остаётся `null`, пока разрешённый источник не отдаст проверяемый полный счётчик.
|
||
|
||
Актуальный внешний ориентир — 19 водоёмов и 252 вида рыб; локальная альфа пока содержит 2+2 сущности. Media-manifest включает 452 кандидата: 228 изображений рыб, 149 приманок и 75 справочных изображений; подтверждённых entity-карт водоёмов пока нет. Вручную проверены и сопоставлены 1 рыба, 2 приманки и 1 официальная схема; 446 файлов остаются в очереди, 1 URL признан невалидным. Полное число «снастей» пока не заявляется: приманки — лишь одна часть каталога наряду с удилищами, катушками, лесками, крючками и оснастками.
|
||
|
||
Для измерений на собственном сервере подготовлен read-only `deploy/load-smoke.py`: он считает p50/p95/max и HTTP-коды для activity/records, а при наличии `ADMIN_TOKEN` — staging/moderation. Методика и безопасные ступени нагрузки описаны в [docs/load-testing.md](docs/load-testing.md); локальные цифры не выдаются за production baseline.
|
||
|
||
В production MinIO root credentials доступны только одноразовому init-контейнеру. API использует отдельного пользователя с доступом исключительно к `S3_BUCKET`: просмотр bucket, чтение, запись и удаление его объектов без глобального списка bucket и без права создавать новые.
|
||
|
||
Production release отделяет Alembic от runtime: одноразовый `migrate` должен успешно завершиться до запуска новой версии API. Перед изменением схемы создаётся backup; совместимый rollback возвращает предыдущие images, несовместимый — восстанавливает предрелизную копию данных вместо непроверенного `alembic downgrade`.
|
||
|
||
Путь обновления схемы проверяется изолированным `deploy/test-release-upgrade.sh`: предыдущая ревизия получает контрольную запись, обновляется до head, после чего проверяются версия, сохранность записи и новые колонки.
|
||
|
||
Тяжёлый production bootstrap вынесен в отдельный ручной/еженедельный CI workflow с 30-минутным timeout и сохраняемыми diagnostics; обычный push по-прежнему использует быстрый Compose E2E.
|
||
|
||
Публичный API зафиксирован генерируемым [OpenAPI-контрактом](docs/api-contract.md): CI сравнивает `apps/api/openapi.json` с фактической схемой FastAPI, поэтому рефакторинг routers не может незаметно изменить URL, параметры или response models. Декомпозиция выполняется инкрементально: catalog, activity/spots, public data и submissions принадлежат отдельным `APIRouter`; проверка доверенных proxy и persistent rate limit отправки улова изолированы в `submission_security`.
|
||
|
||
После повторных ошибок scheduler увеличивает паузу экспоненциально до 24 часов и возвращается к 30 минутам после успеха. Публичная страница `/status` показывает свежесть и состояние источников без URL запросов, внутренних ошибок и другой диагностической информации.
|
||
|
||
Подробный план и актуальные чекбоксы находятся в [`docs/ROADMAP.md`](docs/ROADMAP.md). Результаты проверки интерфейса и пять приоритетных UX-пакетов описаны в [`docs/UI_UX_AUDIT.md`](docs/UI_UX_AUDIT.md).
|
||
|
||
Production-контур для домена `rf4spotter.ru`, TLS, секреты, backup/restore и команды первого запуска описаны в [`deploy/README.md`](deploy/README.md). Он использует отдельный `compose.production.yaml`; локальный `compose.yaml` остаётся средой разработки. Production seed добавляет только справочники — демонстрационные уловы отключены. Изолированные проверки `deploy/test-production-bootstrap.sh` и `deploy/test-backup-restore.sh` подтверждают старт с пустых volumes и восстановление данных.
|
||
|
||
Политика минимизации данных и ежедневная dry-run-first очистка описаны в [`docs/data-retention.md`](docs/data-retention.md).
|
||
|
||
Host-side мониторинг контейнеров, readiness, диска, объёма PostgreSQL/MinIO, резервных копий и TLS описан в [`docs/production-monitoring.md`](docs/production-monitoring.md).
|
||
|
||
Принятые границы стека, memory-cache, scheduler и хранилищ зафиксированы в [архитектурных решениях](docs/architecture-decisions.md). Порядок действий при заполнении диска, отказах PostgreSQL/MinIO, зависшем импорте, ошибке миграции и утечке секрета находится в [incident runbook](docs/incident-runbook.md).
|
||
Ежедневный systemd timer создаёт проверяемую копию до retention-очистки, а production Compose ограничивает рост JSON-логов контейнеров.
|
||
|
||
Фактическое состояние DNS/TLS домена и серверный чек-лист ведутся в [`docs/deployment-status.md`](docs/deployment-status.md).
|
||
Результаты security review и остаточные риски открытой альфы записаны в [`docs/security-review.md`](docs/security-review.md).
|
||
|
||
## Архитектура
|
||
|
||
```text
|
||
Caddy :80/:443
|
||
├─ Astro SSR web
|
||
├─ FastAPI /api и /health
|
||
└─ MinIO: только health и подписанные объекты
|
||
FastAPI ─ PostgreSQL 17
|
||
└ MinIO/S3
|
||
```
|
||
|
||
Наружу production-профиль публикует только Caddy. PostgreSQL, API, Astro и MinIO находятся в Docker-сетях. Caddy завершает TLS и защищает административные страницы Basic Auth; административный API отдельно проверяет Bearer-токен в FastAPI. Basic не накладывается на API-запросы.
|
||
|
||
Gitea Actions workflow `.gitea/workflows/ci.yml` на каждый push и pull request проверяет Python, миграции на чистой PostgreSQL, Astro build и полный Compose/Playwright-сценарий. При падении E2E сохраняются логи контейнеров и Playwright-артефакты.
|
||
|
||
Актуальная инвентаризация источников и правила подключения адаптеров находятся в [`docs/data-source-audit.md`](docs/data-source-audit.md). Разрешённый технический пилот RF4DB/RF4-STAT описан в [`docs/community-source-pilot.md`](docs/community-source-pilot.md), а статус разрешений и лимитов — в [`docs/data-permissions.md`](docs/data-permissions.md). Все наблюдения сначала попадают в staging. Полные записи с уже подтверждёнными алиасами могут публиковаться автоматически; новые соответствия требуют модерации, а неполные записи показываются отдельно и не влияют на индекс.
|
||
|
||
Один ограниченный снимок публичных карточек можно получить исследовательским CLI:
|
||
|
||
```bash
|
||
python -m rf4_research.community_cli rf4db --limit 25
|
||
python -m rf4_research.community_cli rf4stat-fishing --limit 100
|
||
python -m rf4_research.community_cli rf4stat-posts --limit 25
|
||
python -m rf4_research.community_cli rf4map-point --url https://rf4map.ru/points/275 --limit 25
|
||
python -m rf4_research.community_cli rf4posts-spot --url https://rf4-posts.com/ru/spots/UUID --limit 25
|
||
```
|
||
|
||
Команды печатают нормализованный JSON в stdout и ничего не записывают в базу. Detail-команды требуют явный публичный URL и не обходят запрещённые `/api/`. CLI резервирует домен в `.cache/community-fetch-state.json` до HTTP-запроса и блокирует любой его endpoint на 30 минут даже после ошибки. Это автономный исследовательский режим: не запускайте его одновременно с production scheduler; для ручного production-запуска используйте `app.cli fetch-community`, который разделяет PostgreSQL-cooldown с scheduler.
|
||
|
||
Проверенный JSON можно идемпотентно загрузить в изолированный staging, не влияющий на публичную статистику:
|
||
|
||
```bash
|
||
python -m rf4_research.community_cli rf4db --limit 25 \
|
||
| docker compose exec -T api python -m app.cli stage-community-json --input -
|
||
```
|
||
|
||
Staging проверяет происхождение URL и диапазоны значений. Источники локально включаются явно; production-профиль запускает разрешённый scheduler. Ручная очередь доступна по адресу <http://localhost:4321/admin/external-sources>. Автопубликация разрешена только для полных наблюдений с ранее подтверждёнными каноническими соответствиями рыбы и водоёма; остальные записи не обходят модерацию.
|
||
|
||
## Запуск через Docker
|
||
|
||
Требуются Docker Engine и Docker Compose. Это основной и рекомендуемый сценарий:
|
||
|
||
```bash
|
||
docker compose up --build
|
||
```
|
||
|
||
После успешного запуска:
|
||
|
||
- сайт: <http://localhost:4321>;
|
||
- OpenAPI: <http://localhost:8000/docs>;
|
||
- liveness API: <http://localhost:8000/health>;
|
||
- readiness PostgreSQL, MinIO и импорта с версией/revision сборки: <http://localhost:8000/ready>;
|
||
- консоль MinIO: <http://localhost:9001>.
|
||
|
||
Контейнер API сам выполняет `alembic upgrade head`, затем идемпотентный seed. PostgreSQL хранит данные в именованном volume `postgres_data`, а MinIO — в `minio_data`. Compose ожидает readiness PostgreSQL и MinIO перед API, а API-контейнер проверяет `/ready`. Версия и commit SHA задаются через `APP_VERSION`/`APP_REVISION`; те же значения доступны администратору в `/api/v1/admin/diagnostics`. Официальный импорт по умолчанию необязателен; при включённом scheduler установите `OFFICIAL_IMPORT_REQUIRED=true`, тогда отсутствующий, неуспешный или просроченный запуск сделает readiness отрицательным.
|
||
|
||
API и scheduler пишут по одной JSON-записи на событие. HTTP-лог содержит только сгенерированный `request_id`, метод, путь без query string, статус и длительность; IP, заголовок авторизации и пользовательский payload не журналируются. `X-Request-ID` возвращается клиенту. Стандартный access-log Uvicorn отключён. Уровень управляется `LOG_LEVEL`. Публичный агрегат активности кэшируется в памяти процесса на 20 секунд (до 128 ключей) и очищается после публикации, модерации или удаления через этот процесс API; изменения scheduler видны после TTL; `X-Cache` показывает `HIT`/`MISS`. Защищённый `/api/v1/admin/diagnostics` скачивает JSON только с идентификатором сборки и агрегированными счётчиками, без имён игроков, исходных URL, payload и ошибок парсеров.
|
||
|
||
Остановка:
|
||
|
||
```bash
|
||
docker compose down
|
||
```
|
||
|
||
Удаление volume и повторное создание чистой базы — только когда данные больше не нужны:
|
||
|
||
```bash
|
||
docker compose down --volumes
|
||
docker compose up --build
|
||
```
|
||
|
||
Переменные и локальные значения по умолчанию перечислены в [.env.example](.env.example). Секретов в репозитории нет.
|
||
|
||
### Основные переменные окружения
|
||
|
||
| Группа | Переменные |
|
||
|---|---|
|
||
| База | `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `DATABASE_URL` |
|
||
| Домены | `SITE_DOMAIN`, `FILES_DOMAIN`, `ACME_EMAIL` |
|
||
| Администрирование | `ADMIN_TOKEN`, `ADMIN_BASIC_USER`, `ADMIN_BASIC_PASSWORD_HASH` |
|
||
| Объекты | `MINIO_ROOT_USER`, `MINIO_ROOT_PASSWORD`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`, `S3_BUCKET` |
|
||
| Импорт | `OFFICIAL_RECORDS_URL`, `OFFICIAL_RECORDS_REGION`, `OFFICIAL_RECORDS_CATEGORY`, `OFFICIAL_IMPORT_REQUIRED`, `IMPORT_INTERVAL_SECONDS` |
|
||
| Privacy/retention | `RATE_LIMIT_SECRET`, `RETENTION_*_DAYS` |
|
||
| Эксплуатация | `BACKUP_ROOT`, `MONITOR_*`, `LOG_LEVEL` |
|
||
|
||
Полный production-шаблон с комментариями находится в [.env.production.example](.env.production.example). Перед запуском `deploy/preflight.sh` блокирует известные заглушки и ошибочное повторное использование MinIO credentials.
|
||
|
||
## Что реализовано
|
||
|
||
- FastAPI и SQLAlchemy 2;
|
||
- PostgreSQL 17 и миграции Alembic до `20260910_recovery`;
|
||
- идемпотентный seed с двумя точками и свежими демо-уловами;
|
||
- `GET /api/v1/activity` с фильтрами периода, водоёма, рыбы, способа и сортировки;
|
||
- `GET /api/v1/spots/{id}` и `/catches`;
|
||
- справочники рыб, водоёмов и приманок;
|
||
- Astro SSR-интерфейс с адаптивным дизайном из `design-reference` без переноса React/Vinext-стека;
|
||
- объяснимые индексы активности и уверенности по формуле спецификации;
|
||
- документированная формула и детерминированные тесты окон 6/12/24/72 часа;
|
||
- состояния «нет данных» и «источник недоступен»;
|
||
- идемпотентный импорт официальных записей с журналом запусков;
|
||
- публичная страница `/records` с источником и временем последнего импорта;
|
||
- форма `/report`, защищённые admin API и журнал модерации;
|
||
- отдельные состояния ошибки создания заявки и загрузки скриншота; неудачный скриншот можно добавить повторно по ID и одноразовому секрету уже сохранённой заявки;
|
||
- honeypot и постоянный rate limit в PostgreSQL с HMAC-отпечатками вместо исходных IP;
|
||
- скриншоты уловов в MinIO/S3 с проверкой MIME, расширения, размера и фактического содержимого, повторным кодированием и очисткой метаданных;
|
||
- административная очередь `/admin/moderation` с одобрением, отклонением и обезличенным удалением записи с аудитом.
|
||
|
||
Все пользовательские ники и уловы в seed демонстрационные.
|
||
|
||
## Проверка проекта
|
||
|
||
Backend и исследовательский парсер:
|
||
|
||
```bash
|
||
python3 -m venv .venv
|
||
.venv/bin/pip install -r apps/api/requirements-dev.txt
|
||
.venv/bin/pip install -e .
|
||
.venv/bin/pytest -q
|
||
```
|
||
|
||
Актуальное число тестов выводит команда `pytest`; набор включает backend, импорт, расчёт активности и исследовательский парсер.
|
||
|
||
Production-образ API устанавливает только `requirements.txt`; `pytest` подключается отдельно через `requirements-dev.txt` в локальной среде и CI.
|
||
|
||
Frontend:
|
||
|
||
```bash
|
||
cd apps/web
|
||
npm install
|
||
npm run build
|
||
npm audit --omit=dev
|
||
npm run audit:axe
|
||
npm run audit:lighthouse
|
||
```
|
||
|
||
E2E после запуска Compose:
|
||
|
||
```bash
|
||
cd apps/web
|
||
npx playwright install chromium
|
||
npm run test:e2e
|
||
```
|
||
|
||
## Импорт официальных рекордов
|
||
|
||
Однократный контейнерный запуск после старта базы:
|
||
|
||
```bash
|
||
docker compose --profile tools run --rm importer
|
||
```
|
||
|
||
Импорт делает до трёх ограниченных попыток, проверяет DOM-контракт и не удаляет ранее сохранённые данные при сбое. Повторный запуск обновляет совпавшие записи по SHA-256 ключу и не создаёт дубликаты. PostgreSQL advisory lock не допускает параллельный импорт одной source/region/category через admin и scheduler. Расписание реализовано, но намеренно не включается обычным запуском: сначала требуется согласовать допустимость регулярного опроса официального сайта.
|
||
|
||
Ручной административный запуск также доступен через `POST /api/v1/admin/imports/official-records`, журнал — через `GET /api/v1/admin/imports`. Импорт сохраняет HTTP-метаданные и использует `ETag`/`Last-Modified`, когда источник их предоставляет.
|
||
|
||
Планировщик реализован отдельным opt-in профилем и по умолчанию опрашивает источник не чаще одного раза в час:
|
||
|
||
```bash
|
||
docker compose --profile scheduler up -d scheduler
|
||
```
|
||
|
||
Обычный `docker compose up` его не запускает. Не включайте профиль во внешнем окружении, пока условия автоматического сбора не согласованы с владельцем источника; отсутствие `robots.txt` не является разрешением.
|
||
|
||
## Пользовательские уловы и модерация
|
||
|
||
Новая запись из `/report` получает статус `pending` и не участвует в активности до одобрения. Административные методы требуют заголовок `Authorization: Bearer $ADMIN_TOKEN`:
|
||
|
||
```bash
|
||
curl -H "Authorization: Bearer change-me-in-production" \
|
||
"http://localhost:8000/api/v1/admin/catch-reports?status=pending"
|
||
```
|
||
|
||
Перед внешним развёртыванием обязательно замените демонстрационные `ADMIN_TOKEN`, `RATE_LIMIT_SECRET`, `S3_ACCESS_KEY` и `S3_SECRET_KEY`. Форма принимает JPEG, PNG и WebP до 8 МБ; API сверяет MIME и расширение с фактическим форматом, повторно кодирует изображение и удаляет EXIF перед сохранением в MinIO. Модератор получает временную подписанную ссылку через admin API.
|
||
|
||
Если создание записи прошло успешно, а загрузка скриншота завершилась ошибкой, форма сохраняет на один час ID заявки и одноразовый секрет в защищённой `HttpOnly` cookie и предлагает повторить только загрузку изображения. Повторно отправлять сам улов не требуется; один UUID заявки не даёт права изменить чужую запись.
|
||
|
||
Единый dashboard доступен по адресу <http://localhost:4321/admin>, очереди — `/admin/moderation` и `/admin/external-sources`. Dashboard показывает объём очередей, состояние источников и последние импорты без внутренних URL и текстов ошибок. Администратор вводит `ADMIN_TOKEN`; интерфейс держит его только в памяти открытой страницы и не сохраняет в URL или браузерном хранилище. После 15 минут бездействия или ответа API `401` сессия очищается; также доступен явный выход.
|
||
|
||
В production HTML административных страниц дополнительно закрыт Caddy Basic Auth, а API независимо проверяет Bearer-токен. Неуспешные попытки API-входа считаются в БД по HMAC-идентификатору адреса и временно блокируются после десяти ошибок за десять минут; успешная авторизация очищает ошибки клиента. Интерфейс открывает только ссылки со схемой `http` или `https`; данные источника не могут подставить исполняемую URL-схему в ссылку или превью.
|
||
|
||
Администратор может одобрить, отклонить или удалить сообщение. Очередь внешних наблюдений фильтруется на сервере по источнику и полноте, ищет рыбу/водоём и сортируется по свежести либо риску до применения пагинации; риск поднимает неполные и несопоставленные записи. Во время решения вся карточка блокируется; при ошибке введённая причина остаётся на месте, а после успеха интерфейс сообщает результат и переводит фокус к следующей записи. Удаление очищает ник, комментарий, исходную ссылку и объект скриншота, исключает запись из статистики, но сохраняет обезличенный факт действия в журнале аудита.
|
||
|
||
Для карточки с клавиатурным фокусом доступны подсказанные в интерфейсе быстрые клавиши одобрения, сопоставления и публикации. Они не срабатывают в полях ввода; отклонение и удаление требуют явного нажатия кнопки.
|
||
|
||
Административный API внешней очереди возвращает безопасный provenance: время первого и последнего обнаружения, время проверки, отсутствующие поля и только разрешённые скалярные поля исходной записи. Неизвестные ключи и вложенные служебные структуры в ответ не попадают.
|
||
|
||
В карточке внешнего наблюдения provenance доступен в отдельном раскрываемом блоке: временная линия, отсутствующие и исходные разрешённые поля видны до сопоставления и публикации.
|
||
|
||
`GET /api/v1/admin/moderation-history` объединяет историю решений по пользовательским уловам и внешним наблюдениям; последние события видны на dashboard. Ответ содержит только тип и UUID сущности, время, действие, оператора и причину — без ников, исходных URL и parser payload. Dashboard выгружает отдельный `moderation-history-export`: в нём дополнительно исключены UUID, оператор и свободный текст причины, остаются только время, тип, действие и признак необходимости подтверждения.
|
||
|
||
Решения в обеих очередях используют optimistic locking: API возвращает `moderation_version`, а изменяющий запрос обязан прислать увиденное значение. Проверка выполняется под блокировкой строки; если другая вкладка уже решила запись, сервер отвечает `409`, UI обновляет очередь и не перезаписывает более новое решение.
|
||
|
||
## Эксплуатация production
|
||
|
||
- первый запуск, обновление и preflight: [deploy/README.md](deploy/README.md);
|
||
- backup/restore и учебное восстановление: [deploy/README.md](deploy/README.md#6-резервное-копирование-и-восстановление);
|
||
- мониторинг, systemd timer и реакция на сбои: [docs/production-monitoring.md](docs/production-monitoring.md);
|
||
- сроки хранения и очистка: [docs/data-retention.md](docs/data-retention.md);
|
||
- лимиты и планы запросов: [docs/query-performance.md](docs/query-performance.md).
|
||
|
||
Production-логи структурированы в JSON и не содержат query string, IP, заголовков авторизации или пользовательских payload. Docker хранит не более пяти файлов по 10 МБ на сервис. Ежедневное обслуживание выполняет backup до retention и защищено от параллельного запуска.
|
||
|
||
## Известные ограничения альфы
|
||
|
||
- нет пользовательских аккаунтов, OCR, Telegram-бота и уведомлений о клёве;
|
||
- community-источники автоматически включают в активность только полные наблюдения с подтверждёнными external-ID алиасами; fallback alias по имени ещё не используется автопубликацией. Неполные наблюдения видны в «Полевых сигналах», но не влияют на индекс;
|
||
- offset pagination рассчитана на пилотные объёмы, не на бесконечную ленту;
|
||
- локальный Lighthouse production-сборки 12 сентября показал performance 100, LCP 1,66 с, CLS 0,023 и TBT 9 мс; это лабораторный baseline, полевой INP и серверные метрики появятся после размещения;
|
||
- один сервер остаётся точкой отказа, поэтому обязательны внешний backup и мониторинг;
|
||
- текущий публичный DNS/TLS не подтверждён, см. [статус развёртывания](docs/deployment-status.md).
|
||
|
||
## Исследовательский парсер официальных рекордов
|
||
|
||
```bash
|
||
python -m rf4_research.records \
|
||
--url https://rf4game.de/records/region/RU/ \
|
||
--region RU \
|
||
--category records
|
||
```
|
||
|
||
Команда делает один HTTP-запрос и печатает типизированные записи в JSON. Исследовательский и продуктивный адаптеры используют общий fail-closed DOM-парсер `rf4_research/official_parser.py`; продуктивный слой добавляет ограниченные повторы, условные HTTP-запросы, транзакции, дедупликацию и журнал запусков. Подтверждённая структура источника и риски описаны в [docs/data-sources.md](docs/data-sources.md).
|