# 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-наблюдения публикуются сразу в отдельной ленте «Полевые сигналы» с предупреждением и перечнем отсутствующих полей; до подтверждения полноты они не влияют на индекс клёва. Лента раскрывается серверной кнопкой «Показать ещё», сохраняет выбранные фильтры и ограничена 48 сигналами на страницу. Совпадающие рыба, водоём, координаты и вес объединяются визуально, при этом карточка сохраняет все уникальные ссылки на исходные наблюдения. Sidebar лидера скрывается при единственном результате, чтобы не повторять ту же карточку. Базовый SEO-контур готов для `rf4spotter.ru`: страницы имеют уникальные метаданные, canonical, Open Graph/Twitter Card, фирменное изображение 1200×630 и JSON-LD; доступны динамические `/robots.txt` и `/sitemap.xml`, административные и ошибочные страницы закрыты от индексации, добавлена собственная страница 404. Индексируемые каталоги рыб и водоёмов, detail-страницы и сочетания водоём + рыба строятся из актуального разрешённого справочника и включаются в sitemap. Подключены фирменные favicon/app icons, web manifest и production-кэширование статических ресурсов. Карточки активности показывают единый паспорт данных: источники, свежесть, полноту и уровень доверия. На `/status` опубликована легенда цветов всех источников и статусов качества. Публичные точки используют постоянные читаемые адреса вида `/spots/kuori-85x92`; старые UUID-адреса остаются совместимыми и перенаправляются на канонический URL. На странице точки координаты дополнительно показаны фирменным радаром, который не имитирует отсутствующую географию водоёма, а уловы за 72 часа — шкалой-леской с 12-часовым шагом. Каждый улов показывает источник, относительную свежесть и точную дату в подсказке. Карточки активности и каталог дополнены лёгкими SVG-силуэтами рыб без внешних графических зависимостей. Пустые и аварийные состояния используют собственную CSS-иллюстрацию поплавка; анимация учитывает системное ограничение движения. Все пять community-парсеров подключены к отдельному scheduler-процессу. Состояние запусков и ошибок хранится в PostgreSQL, параллельный запуск одного источника блокируется, минимальный интервал жёстко ограничен 1800 секундами. Локально процесс включается профилем `docker compose --profile scheduler up -d`; detail-URL RF4MAP/RF4 Posts задаются переменными окружения. После повторных ошибок 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). Ежедневный 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 и диапазоны значений. Источники по умолчанию выключены; автоматического преобразования в одобренные уловы нет. Ручная очередь доступна по адресу . Публикация разрешена только после сопоставления канонических рыбы и водоёма и при наличии координат и веса. ## Запуск через Docker Требуются Docker Engine и Docker Compose. Это основной и рекомендуемый сценарий: ```bash docker compose up --build ``` После успешного запуска: - сайт: ; - OpenAPI: ; - liveness API: ; - readiness PostgreSQL, MinIO и импорта с версией/revision сборки: ; - консоль MinIO: . Контейнер 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 секунд и очищается после публикации, модерации или удаления; `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 до `0013`; - идемпотентный 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 заявки не даёт права изменить чужую запись. Очередь модерации доступна по адресу . Администратор вводит `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).