- Extend draft recovery to create_error, rate_limited, server_error, timeout - Clear draft only on success (sent/screenshot_sent) - Focus on form-error after recovery - Double submit protection already in place (R10) - Astro check: 0 errors
RF4 Spotter
RF4 Spotter — неофициальный сервис свежих точек и статистики клёва для Russian Fishing 4. Он объединяет публичные рекорды и подтверждённые пользовательские уловы, показывает свежесть источников и рассчитывает объяснимые индексы активности и уверенности. Проект не взаимодействует с игровым клиентом и не является официальным продуктом RF4.
Код и оригинальные материалы проекта распространяются по AGPL-3.0-only. Лицензия не распространяется на игровые материалы, данные сторонних источников и пользовательские загрузки. Правила обработки описаны в политике данных.
Статус разработки
Проверка 10 сентября 2026 (4f68d6b): к деплою пока не готов. Python: 107 passed, 1 skipped; Astro check/build, web unit и Caddy adapt проходят. Исправлены синтаксис Caddy и потребители activity envelope, но остаются цикл readiness/scheduler, неатомарный cooldown, ошибки пагинации/фильтров и другие недоработки. Актуальный план A01–A13, промпт исполнителю. Начинать с A01, затем A02. Bootstrap больше не запускает реальные парсеры, но требует обновления проверки миграции и изолированной proxy/scheduler-приёмки. Ниже описаны реализованные возможности, а не гарантия приёмки текущей ревизии.
Web Docker-образ устанавливает зависимости через npm ci по lock-файлу и удаляет devDependencies после сборки. Локальные .env исключены из web build context.
Очередь внешних наблюдений выбирает только ожидающие проверки записи на сервере и показывает их страницами по 50. Обработанные записи не скрывают более старые необработанные наблюдения.
Запросы Astro к API ограничены таймаутом 8 секунд на запрос. При недоступности API каталоги рыб/водоёмов возвращают HTTP 503 с Retry-After, вместо успешного ответа со страницей ошибки.
В очереди внешних наблюдений доступна кнопка «Подсказать соответствия»: она показывает ранее подтверждённые рыбу и водоём. Значения формы не меняются автоматически; сопоставление и публикация подтверждаются отдельно.
Повторный импорт изменённой опубликованной записи переводит её на ручную проверку и снимает прежний улов с активности (с учётом TTL кэша). После сопоставления и подтверждения обновляется тот же улов; дубликат не создаётся. Автоматическое обнаружение удалённых оригиналов пока не реализовано.
Идёт исправление аудита: актуальные изменения и ограничения перечислены в AUDIT_FIXES.md. Production Compose включает community scheduler; страницы rules/privacy реализованы. Для запуска остаются сервер, DNS/TLS, секреты, внешний backup и контакты. Шкала 72 часов использует полную выборку по времени поступления; одинаковые поля разных источников больше не считаются доказательством одного события. Фоновая публикация обновляет кэш API в пределах TTL, не мгновенно.
Предыдущий полный аудит 8 сентября: отчёт. T01/T02 исправили маршруты формы и конфликт admin-аутентификации; sh deploy/test-proxy-routing.sh проверял Astro redirects, API, Basic/Bearer и отказы на прежней ревизии. Синтаксис Caddy теперь исправен. Подключение scheduler к исходящей сети добавлено; актуальная интеграционная приёмка входит в план A01–A13.
На ширинах 320, 390, 768 и 1280 px ранее проверено отсутствие горизонтального переполнения основных страниц. Это не полная визуальная приёмка: аудит обнаружил неверную desktop-компоновку фильтров; наполненные карточки, длинные названия, клавиатура и zoom остаются отдельной задачей.
Функциональный MVP и локальный production-контур готовятся к открытой альфе: официальный импорт, пользовательские заявки, модерация, объяснимый индекс, staging внешних источников, адаптивный Astro UI, миграции, резервное копирование, retention, мониторинг и security/accessibility-проверки реализованы. На всех страницах подключён компактный баннер открытой альфы со ссылками на статус, правила и отправку улова. В production Compose включён community scheduler; локально он запускается отдельным профилем. Публичный запуск блокируют покупка и настройка сервера, DNS/TLS, реальные секреты, внешний backup, канал уведомлений; публичный адрес обратной связи ещё не задан.
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, web manifest и production-кэширование статических ресурсов. Карточки активности показывают единый паспорт данных: источники, свежесть, полноту и уровень доверия. На /status опубликована легенда цветов всех источников и статусов качества.
Публичные точки используют постоянные читаемые адреса вида /spots/kuori-85x92; старые UUID-адреса остаются совместимыми и перенаправляются на канонический URL. На странице точки координаты дополнительно показаны фирменным радаром, который не имитирует отсутствующую географию водоёма, а уловы за 72 часа — шкалой-леской с 12-часовым шагом. Каждый улов показывает источник, относительную свежесть и точное время UTC; время получения явно отделено от времени улова. Карточки активности и каталог дополнены лёгкими SVG-силуэтами рыб без внешних графических зависимостей. Пустые и аварийные состояния используют собственную 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 задаются переменными окружения.
После повторных ошибок scheduler увеличивает паузу экспоненциально до 24 часов и возвращается к 30 минутам после успеха. Публичная страница /status показывает свежесть и состояние источников без URL запросов, внутренних ошибок и другой диагностической информации.
Подробный план и актуальные чекбоксы находятся в docs/ROADMAP.md. Результаты проверки интерфейса и пять приоритетных UX-пакетов описаны в docs/UI_UX_AUDIT.md.
Production-контур для домена rf4spotter.ru, TLS, секреты, backup/restore и команды первого запуска описаны в 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.
Host-side мониторинг контейнеров, readiness, диска, объёма PostgreSQL/MinIO, резервных копий и TLS описан в docs/production-monitoring.md.
Ежедневный systemd timer создаёт проверяемую копию до retention-очистки, а production Compose ограничивает рост JSON-логов контейнеров.
Фактическое состояние DNS/TLS домена и серверный чек-лист ведутся в docs/deployment-status.md.
Результаты security review и остаточные риски открытой альфы записаны в docs/security-review.md.
Архитектура
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. Разрешённый технический пилот RF4DB/RF4-STAT описан в docs/community-source-pilot.md, а статус разрешений и лимитов — в docs/data-permissions.md. Данные сохраняются только в промежуточный staging и не влияют на индекс без явной проверки и публикации администратором.
Один ограниченный снимок публичных карточек можно получить исследовательским CLI:
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, не влияющий на публичную статистику:
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. Это основной и рекомендуемый сценарий:
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 и ошибок парсеров.
Остановка:
docker compose down
Удаление volume и повторное создание чистой базы — только когда данные больше не нужны:
docker compose down --volumes
docker compose up --build
Переменные и локальные значения по умолчанию перечислены в .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. Перед запуском 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 и исследовательский парсер:
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:
cd apps/web
npm install
npm run build
npm audit --omit=dev
npm run audit:axe
npm run audit:lighthouse
E2E после запуска Compose:
cd apps/web
npx playwright install chromium
npm run test:e2e
Импорт официальных рекордов
Однократный контейнерный запуск после старта базы:
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 профилем и по умолчанию опрашивает источник не чаще одного раза в час:
docker compose --profile scheduler up -d scheduler
Обычный docker compose up его не запускает. Не включайте профиль во внешнем окружении, пока условия автоматического сбора не согласованы с владельцем источника; отсутствие robots.txt не является разрешением.
Пользовательские уловы и модерация
Новая запись из /report получает статус pending и не участвует в активности до одобрения. Административные методы требуют заголовок Authorization: Bearer $ADMIN_TOKEN:
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;
- backup/restore и учебное восстановление: deploy/README.md;
- мониторинг, systemd timer и реакция на сбои: docs/production-monitoring.md;
- сроки хранения и очистка: docs/data-retention.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 показывал LCP 9,3 с; после него hero уменьшен с 1,6 МБ до 71 КБ и получил высокий приоритет загрузки, повторный production-замер выполняется после размещения;
- один сервер остаётся точкой отказа, поэтому обязательны внешний backup и мониторинг;
- текущий публичный DNS/TLS не подтверждён, см. статус развёртывания.
Исследовательский парсер официальных рекордов
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.