This commit is contained in:
ik
2026-09-03 08:18:35 +07:00
parent a7bfc7ee8d
commit d407e8fcfd
103 changed files with 24544 additions and 13 deletions
+703
View File
@@ -0,0 +1,703 @@
# RF4 Spotter — идеи и техническое задание для MVP
> Рабочий документ для передачи в Codex. Его задача — дать Codex достаточно контекста, чтобы начать проектирование и разработку без пересказа всей переписки.
## 1. Идея продукта
**RF4 Spotter** — неофициальный информационный сайт для игроков «Русской Рыбалки 4» (Russian Fishing 4), который отвечает на практический вопрос:
> **Куда мне пойти ловить прямо сейчас, какую снасть или приманку взять и насколько свежа эта информация?**
Сайт должен объединять:
- официальные рекорды RF4;
- пользовательские сообщения об уловах;
- координаты точек;
- приманки, наживки, оснастки и способы проводки;
- время поимки;
- историю активности;
- простой и понятный индекс клёва.
Главная ценность — не вечный справочник старых точек, а **оценка текущей активности с указанием свежести и надёжности данных**.
## 2. Что известно об источниках данных
### Официальные данные
У RF4, по предварительным данным, нет документированного публичного API со всеми уловами и координатами. Однако официальный сайт публикует таблицы рекордов и рейтингов, которые можно разбирать автоматически.
Предположительно из таблиц рекордов можно получать:
- регион;
- категорию рекорда;
- игрока;
- вид рыбы;
- вес;
- водоём;
- приманку или наживку;
- дату.
Исходные точки для исследования:
- официальный раздел рекордов: <https://rf4game.de/records/region/RU/>;
- пример открытого парсера: <https://github.com/hurfy/rf4-api>;
- существующий статистический сервис: <https://rf4-stat.ru/help/>;
- пример пользовательской базы: <https://download.rf4db.com/ru/catches>.
Перед реализацией парсера нужно проверить актуальную HTML-структуру, сетевые запросы страницы и условия использования сайта. Нельзя считать приведённые URL и структуру полей неизменными.
### Пользовательские данные
Официальные таблицы, вероятно, не содержат точных координат, проводки и всех обычных уловов. Их нужно собирать отдельно:
- через собственную форму на сайте;
- позднее — из разрешённых Telegram-, Discord- или VK-источников;
- позднее — из скриншотов с подтверждением распознанных полей пользователем.
### Важное ограничение
В MVP нельзя перехватывать трафик игрового клиента, внедряться в процесс игры, обходить античит или заниматься reverse engineering закрытого протокола. Проект должен работать только с публичными веб-данными и добровольно переданной пользователями информацией.
## 3. Для кого делаем
Основной пользователь — игрок RF4, который не хочет просматривать десятки сообщений в сообществах перед каждой игровой сессией.
Типичный запрос:
```text
Уровень: 19
Способ ловли: спиннинг
Доступные водоёмы: до Куори включительно
Цель: серебро
```
Ожидаемый ответ:
```text
Сейчас активна щука на Вьюнке
Точка: 110:103
Приманка: Spiker #2 01-015
Последнее подтверждение: 34 минуты назад
Уловов за 12 часов: 27 от 11 игроков
Активность: высокая
Уверенность: 87 из 100
```
## 4. Главная гипотеза MVP
Игроку полезнее несколько свежих и объяснимых рекомендаций, чем большая база точек без даты и источника.
MVP считается полезным, если пользователь может:
1. открыть главную страницу;
2. выбрать водоём, рыбу или способ ловли;
3. увидеть активные комбинации «водоём + рыба + точка»;
4. понять, на чём основана оценка;
5. открыть карточку точки с приманками и динамикой;
6. отправить свой улов через простую форму.
## 5. Состав MVP
### 5.1. Главная страница «Что клюёт сейчас»
На странице должны быть:
- фильтр по водоёму;
- фильтр по рыбе;
- фильтр по способу ловли;
- фильтр по периоду: 6, 12, 24, 72 часа;
- сортировка по активности, уверенности и свежести;
- карточки активных точек;
- явное время последнего обновления данных.
Карточка содержит:
- водоём;
- рыбу;
- координаты;
- лучшую приманку или наживку;
- количество уловов за выбранный период;
- количество разных авторов;
- средний и максимальный вес;
- время последнего подтверждения;
- `activity_score` от 0 до 100;
- `confidence_score` от 0 до 100;
- короткое текстовое объяснение оценки.
### 5.2. Страница точки
URL вида:
```text
/spots/vyunok/110-103/pike
```
Содержимое:
- координаты и описание места;
- статистика уловов за 24 часа, 3 дня и 7 дней;
- список наиболее результативных приманок или наживок;
- способы проводки и скорость, если указаны;
- распределение уловов по игровому времени;
- последние подтверждённые уловы;
- источник и свежесть каждой записи;
- предупреждение, если данных мало или они устарели.
### 5.3. Страница официальных рекордов
- таблица импортированных рекордов;
- фильтры по рыбе, водоёму, категории и периоду;
- дата последнего успешного импорта;
- отметка, что это данные официального сайта RF4;
- ссылка на исходную страницу.
### 5.4. Форма добавления улова
Обязательные поля:
- рыба;
- вес;
- водоём;
- координаты `x:y`;
- дата и время отправки.
Необязательные поля:
- приманка или наживка;
- тип оснастки;
- проводка;
- скорость проводки;
- игровое время;
- комментарий;
- имя или ник игрока;
- ссылка на исходную публикацию;
- скриншот.
Для первой версии скриншот хранится как подтверждение, но **не распознаётся автоматически**.
### 5.5. Простая модерация
Минимальная закрытая страница администратора:
- список новых записей;
- просмотр всех полей и скриншота;
- действия «одобрить», «отклонить», «исправить»;
- причина отклонения;
- журнал изменения статуса.
До одобрения пользовательский улов не влияет на публичный индекс клёва.
## 6. Что не входит в MVP
- перехват данных из запущенной игры;
- reverse engineering протокола RF4;
- OCR и компьютерное зрение для скриншотов;
- автоматический сбор всех публикаций Telegram, Discord и VK;
- мобильное приложение;
- сложная персонализация по снастям и бюджету;
- прогнозирование клёва с помощью ML;
- комментарии и социальная сеть;
- полноценная интерактивная карта каждого водоёма;
- автоматическое определение игровых обновлений и миграций рыбы.
Эти возможности допустимы после проверки основного сценария.
## 7. Предлагаемый стек
### Вариант для первого релиза
- **Frontend:** Astro + TypeScript;
- **интерактивные компоненты:** React или Svelte islands только там, где они нужны;
- **стили:** Tailwind CSS либо обычный CSS с дизайн-токенами;
- **Backend API:** Python 3.12 + FastAPI;
- **ORM и миграции:** SQLAlchemy 2 + Alembic;
- **БД:** PostgreSQL;
- **фоновые задания:** APScheduler или отдельный CLI, запускаемый cron;
- **парсер:** сначала `httpx` + BeautifulSoup; Playwright только если обычного HTTP недостаточно;
- **хранение скриншотов:** S3-совместимое объектное хранилище;
- **локальный запуск:** Docker Compose;
- **тесты:** pytest для backend, Vitest для frontend, Playwright для ключевого end-to-end сценария.
На старте Celery и Redis не нужны: периодический импорт можно сделать отдельной идемпотентной CLI-командой.
## 8. Архитектура
```text
Официальный сайт RF4 ──> импортёр рекордов ──┐
Форма пользователя ──> модерация ───────────┼──> PostgreSQL
│ │
Ручной импорт CSV/JSON ──────────────────────┘ │
v
расчёт активности
v
FastAPI JSON API
v
Astro-сайт
```
Предлагаемая структура репозитория:
```text
rf4-spotter/
├── README.md
├── .env.example
├── compose.yaml
├── apps/
│ ├── web/ # Astro
│ └── api/ # FastAPI
├── packages/
│ └── contracts/ # OpenAPI/types, если понадобится
├── data/
│ └── fixtures/ # обезличенные тестовые HTML/JSON
├── docs/
│ ├── RF4_MVP_SPEC.md
│ └── data-sources.md
└── scripts/
└── seed_demo_data.*
```
## 9. Модель данных
Названия таблиц и полей можно скорректировать после прототипа, но модель должна отделять справочники, исходные наблюдения и рассчитанные агрегаты.
### `fish`
| Поле | Тип | Назначение |
|---|---|---|
| `id` | UUID | идентификатор |
| `slug` | text unique | значение для URL |
| `name_ru` | text unique | название рыбы |
| `trophy_weight_g` | integer nullable | порог трофея, если известен |
### `waterbody`
| Поле | Тип | Назначение |
|---|---|---|
| `id` | UUID | идентификатор |
| `slug` | text unique | значение для URL |
| `name_ru` | text unique | название водоёма |
| `unlock_level` | integer nullable | уровень открытия |
### `bait`
Объединённый справочник наживок и искусственных приманок для MVP.
| Поле | Тип | Назначение |
|---|---|---|
| `id` | UUID | идентификатор |
| `name` | text | отображаемое название |
| `normalized_name` | text unique | ключ для сопоставления |
| `kind` | enum | `bait`, `lure`, `unknown` |
### `spot`
| Поле | Тип | Назначение |
|---|---|---|
| `id` | UUID | идентификатор |
| `waterbody_id` | UUID FK | водоём |
| `x` | integer | координата X |
| `y` | integer | координата Y |
| `description` | text nullable | заметка о месте |
Уникальный ключ MVP: `(waterbody_id, x, y)`.
### `catch_report`
| Поле | Тип | Назначение |
|---|---|---|
| `id` | UUID | идентификатор |
| `fish_id` | UUID FK | рыба |
| `spot_id` | UUID FK nullable | точка; у официального рекорда её может не быть |
| `waterbody_id` | UUID FK | водоём хранится явно |
| `bait_id` | UUID FK nullable | приманка или наживка |
| `weight_g` | integer | вес в граммах |
| `rig_type` | text nullable | тип оснастки |
| `retrieve_method` | text nullable | способ проводки |
| `retrieve_speed` | integer nullable | скорость проводки |
| `game_time` | time nullable | игровое время |
| `caught_at` | timestamptz nullable | время поимки, если известно |
| `reported_at` | timestamptz | время попадания в систему |
| `player_name` | text nullable | ник игрока |
| `source_type` | enum | `official_record`, `user`, `manual_import` |
| `source_url` | text nullable | ссылка на источник |
| `source_external_id` | text nullable | ключ для дедупликации |
| `source_confidence` | smallint | 0100 |
| `moderation_status` | enum | `pending`, `approved`, `rejected` |
| `screenshot_key` | text nullable | ключ объекта в хранилище |
| `raw_payload` | jsonb nullable | исходные данные импортёра |
### `official_record_import`
| Поле | Тип | Назначение |
|---|---|---|
| `id` | UUID | запуск импортёра |
| `started_at` | timestamptz | начало |
| `finished_at` | timestamptz nullable | завершение |
| `status` | enum | `running`, `success`, `partial`, `failed` |
| `source_url` | text | источник |
| `rows_seen` | integer | найдено строк |
| `rows_created` | integer | добавлено записей |
| `rows_updated` | integer | обновлено записей |
| `error_summary` | text nullable | краткая ошибка |
### `moderation_event`
Хранит историю решений по пользовательскому сообщению: кто, когда, какой статус установил и почему.
## 10. Нормализация и дедупликация
Это критическая часть проекта.
Нужно:
- хранить исходное значение из источника в `raw_payload`;
- нормализовать пробелы, регистр, дефисы и десятичные разделители;
- вести таблицу алиасов для рыб, водоёмов и приманок;
- вес всегда приводить к граммам;
- время хранить с явным указанием, реальное оно или игровое;
- официальный импорт делать идемпотентным;
- не считать две одинаковые строки двумя независимыми подтверждениями.
Для официальных рекордов ключ дедупликации можно сначала строить из нормализованной комбинации:
```text
region + category + player + fish + weight_g + waterbody + bait + record_date
```
Этот ключ следует хешировать и хранить в `source_external_id`.
## 11. Индекс активности
### Требование
Алгоритм MVP должен быть простым, детерминированным и объяснимым. Нельзя показывать псевдоточную оценку без расшифровки.
Расчёт производится для группы:
```text
водоём + точка + рыба + выбранный временной интервал
```
Предлагаемый первый вариант:
```text
freshness_i = exp(-age_hours_i / 18)
weighted_reports = sum(freshness_i * source_confidence_i / 100)
unique_players = количество уникальных непустых player_name
trophy_bonus = min(1, trophy_count / 3)
activity_raw =
55 * min(1, weighted_reports / 12)
+ 25 * min(1, unique_players / 6)
+ 20 * trophy_bonus
activity_score = round(activity_raw)
```
Начальная оценка уверенности:
```text
confidence_score = round(
45 * min(1, approved_reports / 10)
+ 35 * min(1, unique_players / 5)
+ 20 * average_source_confidence / 100
)
```
Правила отображения:
- меньше трёх одобренных пользовательских наблюдений — пометка «данных мало»;
- нет уловов за 72 часа — пометка «данные устарели»;
- один игрок не может создать высокую уверенность большим количеством сообщений;
- официальный рекорд без координат нельзя автоматически приписывать конкретной точке;
- значения 0–100 — сравнительные индексы сервиса, а не вероятность поймать рыбу.
Карточка должна объяснять результат, например:
```text
Высокая активность: 18 свежих уловов от 7 игроков.
Последнее подтверждение 42 минуты назад.
Уверенность средняя: часть сообщений без скриншотов.
```
Формулу после накопления реальных данных нужно откалибровать.
## 12. API MVP
Публичные методы:
```http
GET /api/v1/activity
GET /api/v1/spots/{spot_id}
GET /api/v1/spots/{spot_id}/catches
GET /api/v1/records
GET /api/v1/fishes
GET /api/v1/waterbodies
GET /api/v1/baits
POST /api/v1/catch-reports
```
Пример фильтров активности:
```http
GET /api/v1/activity?waterbody=vyunok&fish=pike&hours=24&method=spinning
```
Административные методы:
```http
GET /api/v1/admin/catch-reports?status=pending
PATCH /api/v1/admin/catch-reports/{id}
POST /api/v1/admin/imports/official-records
GET /api/v1/admin/imports
```
Все списочные методы должны иметь пагинацию, предсказуемую сортировку и валидацию фильтров.
## 13. Импорт официальных рекордов
Импортёр должен быть отдельным адаптером, чтобы изменение вёрстки RF4 не затронуло остальное приложение.
Интерфейс адаптера:
```python
class OfficialRecordsSource(Protocol):
async def fetch(self, region: str, category: str) -> list[RawRecord]: ...
```
Этапы:
1. загрузить страницу или JSON/XHR, если он существует;
2. сохранить диагностические метаданные ответа;
3. разобрать строки в `RawRecord`;
4. нормализовать значения;
5. вычислить ключ дедупликации;
6. добавить или обновить записи транзакционно;
7. записать результат запуска в `official_record_import`.
Требования:
- таймауты и повторы с ограничением;
- понятный User-Agent с названием проекта и контактным URL, когда он появится;
- умеренная частота запросов;
- кэширование;
- фикстуры реальных HTML-ответов для unit-тестов;
- отказ с понятной ошибкой, если ожидаемые колонки исчезли;
- никакого молчаливого импорта перепутанных полей;
- возможность запустить командой вроде `python -m app.cli import-records`;
- расписание не чаще необходимого; начать с одного раза в 30–60 минут.
Если официальный сайт отдаёт данные через внутренний JSON/XHR endpoint, нужно предпочесть его HTML-парсингу, но только если он доступен без обхода защиты.
## 14. Интерфейс и визуальный принцип
Сайт должен выглядеть как современный полезный инструмент, а не как форум или перегруженная игровая база.
Принципы:
- тёмная и светлая темы допустимы, но первая версия может иметь одну хорошо сделанную тему;
- важнее всего: рыба, водоём, точка, свежесть и рабочая приманка;
- цветовая шкала активности не должна быть единственным носителем смысла;
- на мобильном карточки должны читаться без горизонтальной прокрутки;
- таблица рекордов на мобильном превращается в карточки;
- возле любой оценки показывать, на каких данных она основана;
- не смешивать реальное время и игровое время;
- не выдавать старую точку за текущий клёв.
Минимальные состояния каждой страницы:
- загрузка;
- данные есть;
- данных нет;
- данных мало;
- источник временно недоступен;
- ошибка валидации.
## 15. Безопасность и приватность
- не принимать исполняемые файлы;
- проверять MIME-тип, расширение и размер скриншотов;
- удалять EXIF из загруженных изображений;
- генерировать серверные имена объектов;
- ограничить частоту отправки формы;
- добавить honeypot или CAPTCHA после появления спама;
- экранировать пользовательский текст;
- не публиковать IP и технические идентификаторы;
- ник игрока сделать необязательным;
- предусмотреть удаление пользовательского сообщения;
- административную часть защитить аутентификацией;
- секреты хранить только в переменных окружения;
- не коммитить `.env` и реальные скриншоты пользователей.
## 16. Критерии готовности MVP
MVP готов, когда:
- проект поднимается одной документированной командой через Docker Compose;
- миграции создают чистую БД;
- seed-команда добавляет демонстрационные водоёмы, рыб, приманки и уловы;
- импортёр получает или разбирает официальные рекорды из актуального источника;
- повторный импорт не создаёт дубликаты;
- сбой источника не удаляет ранее загруженные данные;
- главная страница показывает активные точки и фильтруется;
- карточка точки объясняет активность и уверенность;
- пользователь может отправить улов;
- администратор может его одобрить или отклонить;
- одобренный улов появляется в публичной статистике;
- есть автоматические тесты главного сценария;
- README описывает запуск, настройку, импорт, тесты и ограничения проекта.
## 17. Этапы разработки
### Этап 0. Исследование источника
- проверить официальный сайт RF4 и его сетевые запросы;
- зафиксировать доступные регионы и категории;
- проверить актуальность проекта `hurfy/rf4-api`;
- сохранить небольшие HTML/JSON-фикстуры;
- описать риски и ограничения в `docs/data-sources.md`;
- не писать весь продукт, пока не доказано, что хотя бы один источник стабильно разбирается.
Результат: маленький исследовательский скрипт и документ с подтверждённой структурой данных.
### Этап 1. Каркас и демонстрационные данные
- создать монорепозиторий;
- настроить Astro, FastAPI, PostgreSQL и миграции;
- создать модель данных;
- добавить seed;
- реализовать read-only API;
- сверстать главную и страницу точки на демоданных.
### Этап 2. Официальные рекорды
- реализовать адаптер источника;
- добавить нормализацию и дедупликацию;
- добавить журнал запусков;
- сделать страницу рекордов;
- добавить периодический запуск.
### Этап 3. Пользовательские уловы
- форма;
- загрузка скриншота;
- модерация;
- rate limit;
- включение одобренных записей в статистику.
### Этап 4. Индекс клёва
- агрегаты;
- активность и уверенность;
- человекочитаемое объяснение;
- тесты на свежесть, дубликаты и вклад разных игроков.
### Этап 5. Пилот
- наполнить базу небольшим набором реальных данных с разрешёнными источниками;
- дать нескольким игрокам протестировать сценарий;
- собрать обратную связь;
- только после этого решать, нужен ли OCR и импорт сообществ.
## 18. Идеи после MVP
Приоритет определять по реальному использованию:
- распознавание скриншота с обязательным подтверждением пользователем;
- Telegram-бот для отправки улова;
- импорт из разрешённых каналов и сообществ;
- персональный профиль: уровень, открытые водоёмы, снасти, бюджет;
- режим «куда пойти прямо сейчас»;
- сравнение приманок;
- динамика клёва по игровому времени;
- обнаружение смены рабочих точек после обновлений;
- уведомление, когда активизировалась выбранная рыба;
- карта водоёма;
- публичный API для сообщества;
- репутация источников и авторов;
- мультиязычность и поддержка разных регионов RF4.
## 19. Открытые вопросы
Codex не должен молча принимать решения по этим пунктам, если они становятся блокирующими:
1. Как будет называться проект и какой домен использовать?
2. Нужна ли авторизация обычных пользователей в первом релизе?
3. Где размещать приложение и объектное хранилище?
4. Можно ли использовать реальные ники игроков в публичной выдаче?
5. Какие именно официальные категории рекордов импортировать первыми?
6. Какие водоёмы и рыбы нужны в пилотной базе?
7. Какая лицензия будет у кода и данных?
8. Разрешают ли правила и `robots.txt` выбранную частоту автоматического сбора?
Для начала разработки допустимы безопасные значения по умолчанию:
- рабочее название `RF4 Spotter`;
- без регистрации обычных пользователей;
- один регион RU;
- одна или две основные категории рекордов;
- локальное S3-совместимое хранилище MinIO;
- демонстрационные ники и данные в seed;
- импорт раз в 60 минут.
## 20. Как работать над проектом в Codex
1. Создать пустой репозиторий и положить этот файл в `docs/RF4_MVP_SPEC.md`.
2. Открыть корень репозитория в Codex.
3. Дать Codex сначала исследовательскую задачу из блока ниже.
4. Попросить фиксировать решения в `docs/` и обновлять README.
5. Делить работу на небольшие проверяемые этапы, а не просить сразу «сделать весь сайт».
6. После каждого этапа просить запускать тесты и показывать, что именно готово.
7. Не передавать Codex пароли, токены и игровые учётные данные; использовать `.env.example`.
### Первый промпт для Codex
```text
Прочитай docs/RF4_MVP_SPEC.md целиком. Пока не создавай весь сайт.
Выполни только этап 0 — исследование источников официальных рекордов RF4.
Задачи:
1. Изучи текущую структуру официальной страницы рекордов и её сетевые запросы.
2. Проверь, существует ли доступный JSON/XHR endpoint, который можно использовать без обхода защиты.
3. Изучи архитектурные идеи проекта https://github.com/hurfy/rf4-api, но не копируй код вслепую и проверь его актуальность.
4. Создай минимальный исследовательский скрипт, который получает одну страницу/категорию и преобразует записи в типизированную внутреннюю структуру.
5. Добавь обезличенную небольшую фикстуру и unit-тест парсера.
6. Создай docs/data-sources.md: подтверждённые поля, URL, параметры, ограничения, рекомендуемую частоту опроса и риски поломки.
7. Не исследуй сетевой протокол игры, не запускай игровой клиент и не обходи защиту сайтов.
Перед изменениями изучи репозиторий и предложи краткий план. После работы запусти тесты и сообщи, что подтверждено фактически, а что осталось предположением.
```
### Второй промпт после успешного исследования
```text
Прочитай docs/RF4_MVP_SPEC.md и docs/data-sources.md. Реализуй этап 1: каркас приложения на Astro + TypeScript, FastAPI, PostgreSQL, SQLAlchemy 2 и Alembic.
Требования:
- локальный запуск через Docker Compose;
- модели и миграции для основных сущностей MVP;
- seed с демонстрационными данными;
- read-only API для активности, точек и справочников;
- главная страница и страница точки;
- адаптивная вёрстка;
- тесты backend и одного главного пользовательского сценария;
- README с точными командами запуска и проверки.
Используй данные только из seed: реальный импортёр будет отдельным этапом. Сначала предложи план и список файлов, затем реализуй и проверь результат.
```
## 21. Короткое описание проекта для README
```text
RF4 Spotter — неофициальный сервис свежих точек и статистики клёва для Russian Fishing 4. Он объединяет публичные рекорды и подтверждённые пользовательские уловы, показывает свежесть источников и рассчитывает объяснимые индексы активности и уверенности. Проект не взаимодействует с игровым клиентом и не является официальным продуктом RF4.
```
## 22. Главное продуктовое правило
> Если данных недостаточно или они устарели, сайт должен честно сказать об этом. «Не знаем» полезнее, чем красивая, но выдуманная рекомендация.