Files
rf4-spotter/README.md
T
2026-09-07 16:43:12 +07:00

238 lines
23 KiB
Markdown
Raw 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.
# RF4 Spotter
RF4 Spotter — неофициальный сервис свежих точек и статистики клёва для Russian Fishing 4. Он объединяет публичные рекорды и подтверждённые пользовательские уловы, показывает свежесть источников и рассчитывает объяснимые индексы активности и уверенности. Проект не взаимодействует с игровым клиентом и не является официальным продуктом RF4.
Код и оригинальные материалы проекта распространяются по [AGPL-3.0-only](LICENSE). Лицензия не распространяется на игровые материалы, данные сторонних источников и пользовательские загрузки. Правила обработки описаны в [политике данных](docs/data-policy.md).
## Статус разработки
Функциональный MVP и локальный production-контур готовятся к открытой альфе: официальный импорт, пользовательские заявки, модерация, объяснимый индекс, staging внешних источников, адаптивный Astro UI, миграции, резервное копирование, retention, мониторинг и security/accessibility-проверки реализованы. Автоматические импорты внешних источников выключены. Публичный запуск блокируют покупка и настройка сервера, DNS/TLS, реальные секреты, внешний backup, канал уведомлений и публичные страницы правил/privacy.
RF4DB/RF4-STAT/RF4MAP/RF4 Posts сначала принимаются в изолированный staging. Полные записи с ранее подтверждёнными алиасами источника публикуются автоматически; новые соответствия и неполные записи остаются на ручной проверке. Для разрешённых community-источников действует интервал не менее 30 минут на источник. Открытая альфа не использует продуктовый allowlist: интерфейс показывает весь корректно загруженный разрешённый каталог, сохраняя требования полноты и модерации.
На сайте у каждой записи отображается источник, а у агрегированной активности — все вошедшие в расчёт источники. Неполные community-наблюдения публикуются сразу в отдельной ленте «Полевые сигналы» с предупреждением и перечнем отсутствующих полей; до подтверждения полноты они не влияют на индекс клёва.
Базовый SEO-контур готов для `rf4spotter.ru`: страницы имеют уникальные метаданные, canonical, Open Graph/Twitter Card, фирменное изображение 1200×630 и JSON-LD; доступны динамические `/robots.txt` и `/sitemap.xml`, административные и ошибочные страницы закрыты от индексации, добавлена собственная страница 404. Индексируемые каталоги рыб и водоёмов, detail-страницы и сочетания водоём + рыба строятся из актуального разрешённого справочника и включаются в sitemap.
Публичные точки используют постоянные читаемые адреса вида `/spots/kuori-85x92`; старые UUID-адреса остаются совместимыми и перенаправляются на канонический 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, диска, резервных копий и TLS описан в [`docs/production-monitoring.md`](docs/production-monitoring.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 token.
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/`. Для RF4-STAT действует пауза не менее пяти секунд между разными страницами; для RF4MAP/RF4 Posts CLI хранит состояние в `.cache/community-fetch-state.json` и блокирует повтор того же источника раньше 30 минут.
Проверенный 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 и диапазоны значений. Источники по умолчанию выключены; автоматического преобразования в одобренные уловы нет. Ручная очередь доступна по адресу <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 и импорта: <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`. Официальный импорт по умолчанию необязателен; при включённом scheduler установите `OFFICIAL_IMPORT_REQUIRED=true`, тогда отсутствующий, неуспешный или просроченный запуск сделает readiness отрицательным.
API и scheduler пишут по одной JSON-записи на событие. HTTP-лог содержит только сгенерированный `request_id`, метод, путь без query string, статус и длительность; IP, заголовок авторизации и пользовательский payload не журналируются. `X-Request-ID` возвращается клиенту. Стандартный access-log Uvicorn отключён. Уровень управляется `LOG_LEVEL`.
Остановка:
```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 до `0012`;
- идемпотентный 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.txt
.venv/bin/pip install -e .
.venv/bin/pytest -q
```
Актуальное число тестов выводит команда `pytest`; набор включает backend, импорт, расчёт активности и исследовательский парсер.
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 заявки не даёт права изменить чужую запись.
Очередь модерации доступна по адресу <http://localhost:4321/admin/moderation>. Администратор вводит `ADMIN_TOKEN`; интерфейс держит его только в памяти открытой страницы и не сохраняет в URL или браузерном хранилище.
Администратор может одобрить, отклонить или удалить сообщение. Удаление очищает ник, комментарий, исходную ссылку и объект скриншота, исключает запись из статистики, но сохраняет обезличенный факт действия в журнале аудита.
## Эксплуатация 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-источники автоматически публикуют только полные наблюдения с подтверждёнными алиасами; новые соответствия требуют ручной проверки, неполные наблюдения не публикуются;
- offset pagination рассчитана на пилотные объёмы, не на бесконечную ленту;
- Lighthouse LCP локального прогона — 9,3 с; оптимизация изображения/CDN остаётся после размещения;
- один сервер остаётся точкой отказа, поэтому обязательны внешний 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).