Files
x0gp/server/PLAN.md
T
x0gp 978d36c505 feat(server): initial implementation
This commit captures the full server code accumulated across
several development sessions. It includes the following layers,
applied in roughly this order during development:

1. Base infrastructure
   - cmd/poc-server HTTP+WebSocket server, /health, /stats
   - internal/transport wire types (Envelope + all message types)
   - internal/catalog, internal/realtime, internal/stats
   - internal/storage/postgres with migrations 001-004
   - .env, .env.example, docker-compose, Dockerfile, scripts/

2. Swagger documentation
   - go get github.com/swaggo/http-swagger
   - swag init produces docs/docs.go
   - /swagger/index.html UI, /swagger/doc.json spec
   - annotations on health/stats/ws and catalog handlers

3. Races API
   - internal/races/{store,service,keyset,types}.go
   - migration 005_races.sql: finished_races, race_plans, race_queue
   - GET /api/races with keyset pagination, status filter
   - GET /api/races/upcoming
   - POST /api/races/queue/join, GET, DELETE
   - POST/GET /api/races/plans, DELETE /{id}
   - lobby.RaceMeta simplification (no host_id/host_name)

4. Races seeder
   - internal/races/seed with deterministic generator
   - --seed-races / --reset CLI flags in main.go
   - 30 finished + 5 live + 5 plans + 4 queue entries

5. Drivers and clans
   - internal/drivers, internal/clans (CRUD, validation)
   - migration 008_drivers_clans.sql
   - /api/clans and /api/drivers endpoints
   - 3-letter uppercase nickname/tag validation
   - lobby.DriverMeta: Nickname, AvatarURL, ClanID, ClanTag

6. Podium
   - internal/transport.RacePodiumEntry
   - lobby.SetDriverProfile for in-memory metadata sync
   - migration 007_podium.sql (podium JSONB column)
   - seeder populates top-3 per finished race

7. Live races persistence
   - migration 009_live_persistence.sql: live_races,
     live_race_drivers, lobby_drivers
   - internal/races/live_store.go: LiveStore with write-side
     mirror for lobby.Service mutations
   - Service.collectLive and Upcoming read from Postgres
   - main.go RestoreFromDB rehydrates lobby on startup

8. Unify live + finished into one races table
   - migration 010_unify_races.sql: rename finished_races to
     races, expand status CHECK, merge live rows
   - PgStore now hosts both write paths; LiveStore is a
     thin facade implementing lobby.Persistence
   - seeder resetAll drops only finished/cancelled rows and
     race_plans / race_queue / lobby_drivers

Each layer is consistent on its own; cross-layer changes are
visible in the file history. A future refactor may split this
commit into the per-stage boundaries listed above.
2026-06-22 22:01:09 +04:00

503 lines
25 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.
# x0gp — Server Plan (Go)
Документ фиксирует серверную архитектуру: транспорт, модули, протокол,
хранение, биллинг, деплой.
Дата фиксации архитектурных решений: 2026-06-18.
Дата обновления (домашний сервер для PoC и MVP): 2026-06-18.
Дата обновления v2 (Orange Pi 5 Pro + Coolify + статический IP): 2026-06-18.
Архитектурные выборы, на которых основан план:
1. Транспорт клиент-сервер: **WebSocket + бинарный protobuf**.
2. Управление машинками: **Wi-Fi, ESP32-S3 агент**.
3. Модель доверия: **authoritative server + client-side prediction**.
4. Позиционирование: **IR-магнитные секторы + overhead-CV (гибрид)**.
---
## 1. Цели и не-цели
### 1.1. Цели
- Round-trip "команда на экране -> отклик машинки" в худшем случае 200 мс,
в типичном 80-120 мс.
- Поддержка одновременно 50-100 активных заездов на 1 Edge-инстанс.
- Устойчивость к обрывам связи (клиент теряет сеть на 0.5-2 с без потери
состояния).
- Anti-cheat: защита от подмены телеметрии, авто-команд, грязного руления.
- Биллинг заездов и турниров (ЮKassa для РФ, Stripe для остального).
- Горизонтальное масштабирование (stateless API, sticky-сессии на WS).
### 1.2. Не-цели (для MVP)
- Физика шин (скорость не считаем, считаем положение с CV).
- Запись/воспроизведение VR-реплеев.
- Полноценный matchmaking с ELO (только базовый лобби).
- Мобильное приложение (только Web, мобилка во второй очереди).
---
## 2. Высокоуровневая архитектура
```
+----------------------+
| React (Web client) |
| React Native (mobile)|
+----------+-----------+
|
WebSocket (binary protobuf)
|
+----------v-----------+
| Edge / API Gateway |
| (Go, stateless) |
+----+------+-----+----+
| | |
+-------------+ | +-------------+
| | |
+------v------+ +--------v-------+ +------v------+
| Race Engine | | Realtime Hub | | REST API |
| (stateful) | | (WS fan-out) | | (chi/echo) |
+-------------+ +----------------+ +-------------+
| | |
+---------+---------+ |
| |
+--------v--------+ |
| Redis | pub/sub, state |
+-----------------+ |
| |
+--------v--------+ |
| Postgres / TS |<-------------------+
+-----------------+
```
**Edge / API Gateway** — единая точка входа, TLS, rate-limit, auth, маршрутизация.
**Race Engine** — authoritative state гонки, tick 60 Hz.
**Realtime Hub** — WebSocket-концентратор, fan-out игрокам, сериализация.
**REST API** — управление пользователями, лобби, платежами, история.
---
## 3. Транспорт
### 3.1. WebSocket (основной)
- Библиотека: **gorilla/websocket** или **nbio** (последняя — non-blocking,
лучше для тысяч соединений).
- Формат кадров: **бинарный protobuf** (для latency) + опционально JSON для
не-real-time сообщений (чат лобби).
- Subprotocol: `x0gp.v1`.
- Ping/pong: каждые 5 с, читаем RTT для диагностики.
### 3.2. Альтернатива на будущее
- WebRTC DataChannel — если потребуется суб-50 мс jitter.
- Собственный UDP+KCP — если появится конкурентная потребность.
- В плане не реализуем, но держим "port" в протоколе для миграции.
### 3.3. REST API
- HTTP/2, chi router.
- OpenAPI 3.1 спецификация, кодогенерация клиента.
- Авторизация: JWT (access 15 мин) + refresh token (30 дней).
- Idempotency-Key для платёжных эндпоинтов.
---
## 4. Структура Go-модулей
```
server/
cmd/
api-gateway/ # REST + WS, stateless
race-engine/ # stateful, authoritative tick
agent-linker/ # запускается в помещении (на gateway-ПК)
indexer/ # пишет телеметрию в Timescale
replay-recorder/ # (опц.) пишет replay в S3
internal/
auth/ # JWT, сессии, refresh, MFA
billing/ # ЮKassa/Stripe, биллинг сессий
race/ # сессии, круги, DNF, рестарты, секторы
realtime/ # WS hub, fan-out, per-client очереди
transport/ # protobuf-схемы, кодеки
control/ # приём команд, rate-limit, prediction snapshots
prediction/ # client-side prediction серверные корректировки
telemetry/ # агрегация, пересылка в TS
anti_cheat/ # эвристики: sanity check, повторы, geo
tournament/ # bracket, лэддер, регистрация
storage/
postgres/ # sqlx/pgx, миграции
redis/ # сессии, pub/sub, лобби
timescale/ # телеметрия
obs/ # логирование, метрики, трейсы
proto/
client_server.proto # команды, телеметрия для игрока
server_agent.proto # команды и состояние для машинки
api/
openapi.yaml
migrations/ # SQL
deploy/
docker-compose.yml # dev
helm/ # prod
scripts/
loadtest/ # 100 виртуальных гонщиков
```
---
## 5. Протокол (protobuf)
### 5.1. Клиент -> Сервер
- `ClientHello { version, token, locale }`
- `JoinRace { raceId, carClass }`
- `LeaveRace {}`
- `InputState { seq, tsMs, steering (-1..1), throttle (0..1), brake (0..1), gear (-1..0..1), buttons }` (60-120 Hz)
- `ChatMessage { text }` (rate-limited)
- `Ping { tsMs }`
### 5.2. Сервер -> Клиент
- `ServerHello { sessionId, raceTick, config }`
- `RaceSnapshot { tick, tsMs, cars: [{id, x, y, heading, speed, lap, sector, lastLapMs}], leaderboard }` (30-60 Hz)
- `InputAck { seq, appliedAtMs }`
- `Correction { deltaSteering, deltaThrottle, reason }` (редко, при расхождении)
- `RaceEvent { type: START|LAP|SECTOR|DNF|END, payload }`
- `TournamentUpdate { ... }`
- `Pong { tsMs }`
### 5.3. Сервер -> Бортовой агент
- `AgentCommand { carId, steering, throttle, brake, gear, flags, expiresAtMs }` (60-120 Hz)
- `AgentConfig { maxSteeringRate, maxThrottleRate, rampLimits }`
### 5.4. Бортовой агент -> Сервер
- `AgentHello { carId, fwVersion, hwId, rssi }`
- `AgentTelemetry { carId, tsMs, steeringActual, throttleActual, rpm, voltage, current, tempEsc, tempMotor, imu: {ax, ay, az, gx, gy, gz}, irSensor: {front, rear}, batteryPct }` (10-30 Hz)
- `Heartbeat { tsMs, uptimeSec, errors }` (1 Hz)
- `AgentLog { level, msg }`
### 5.5. Gateway-агент -> Edge-сервер
- `TrackHello { trackId, hwId, publicKey, version }`
- `CarFrame { carId, tsMs, position: {x,y,z}, heading, velocity, confidence }` (из CV, 30-60 Hz)
- `SectorEvent { carId, sector, tsMs, lap }`
- `ReplayChunk { raceId, tsMs, jpegBytes }` (по запросу)
- `LocalCommand { carId, payload }` (forwarded from server)
---
## 6. Race Engine (authoritative tick)
### 6.1. Tick
- Фиксированный шаг: **16.67 мс (60 Hz)**.
- На каждом тике:
1. Принять все InputState от игроков за последние N мс (батч).
2. Применить к authoritative state машинок (с учётом ramp limits).
3. Сформировать команды бортовым агентам.
4. Отправить команды агенту в помещении.
5. Через ~33 мс получить телеметрию и CV-обновления.
6. Скорректировать state.
7. Сформировать RaceSnapshot для клиентов.
### 6.2. Client-side prediction
- Клиент хранит последний применённый state и локально симулирует физику
между snapshot-ами.
- При получении Snapshot клиент сверяет, если расхождение > порога — плавная
коррекция (lerp 100-200 мс), без "телепорта".
- Сервер шлёт `Correction` только если расхождение > критического.
### 6.3. Anti-cheat эвристики
- Sanity check входящих команд (steering/throttle в допустимом диапазоне,
скорость изменения не выше физического предела).
- Сравнение CV-позиции с заявленной (на сервере) — если расходятся > N метров,
пометка DQF.
- Повторы replay: после заезда сверяем логгированные команды с CV-треком.
- Device fingerprint + поведенческий профиль.
- Rate limit: > 200 Hz InputState = бан.
### 6.4. Состояния гонки
- `LOBBY` -> `COUNTDOWN` (3..1) -> `RACING` -> `FINISHED`.
- DNF по: таймаут без input 5 с, потеря связи с агентом > 3 с, флаг
администратора.
- RESTART: только на LoBBY или между раундами.
---
## 7. Realtime Hub
- Один WS-коннект на игрока.
- Подписки: `race:{raceId}`, `tournament:{id}`, `user:{id}`.
- Fan-out: при Snapshot race-движок публикует в Redis pub/sub, Hub читает
и шлёт подписанным клиентам.
- Backpressure: если клиент не успевает читать — дропаем старые Snapshot,
шлём только последний.
- Sticky-сессии: cookie `x0gp_sticky` при балансировке на L7.
---
## 8. Хранение
### 8.1. PostgreSQL (relational)
Таблицы (основные):
- `users` (id, email, phone, password_hash, created_at, locale)
- `sessions` (id, user_id, refresh_hash, ip, ua, expires_at)
- `wallets` (user_id, balance, currency)
- `payments` (id, user_id, provider, ext_id, amount, status, created_at)
- `races` (id, track_id, class, started_at, ended_at, winner_id, replay_url)
- `race_results` (race_id, user_id, car_id, lap_times[], best_lap, dnf_reason)
- `tournaments` (id, name, format, entry_fee, prize_pool, status)
- `tournament_entries` (tournament_id, user_id, seed, eliminated_at)
- `cars` (id, hw_id, model, fw_version, status, last_seen)
- `tracks` (id, name, layout_id, geo_city, status)
Миграции: `golang-migrate/migrate`.
### 8.2. Redis
- `session:{token}` -> userId (TTL 15 мин)
- `refresh:{token}` -> userId (TTL 30 дн)
- `race:{id}:state` -> serialized race state (TTL 1 час)
- `lobby:{id}` -> set of userIds
- `ratelimit:{user}:{op}` -> token bucket
- pub/sub channel `race_events`
### 8.3. TimescaleDB (time-series)
- hypertable `telemetry` (ts, race_id, car_id, field, value)
- 7 дней горячих данных, потом cold в S3.
- continuous aggregates для "лучший круг на треке", "топ-скорости".
---
## 9. Биллинг
### 9.1. Провайдеры
- **ЮKassa** — для РФ.
- **Stripe** — для остального мира (фаза 3).
### 9.2. Сценарии
- Пополнение кошелька (one-time payment).
- Списание за заезд (раз в N минут или за сессию).
- Входной взнос в турнир.
- Призовые начисления победителям.
### 9.3. Идемпотентность и реконсиляция
- Каждый платёж имеет `Idempotency-Key` на стороне клиента и `ext_id` от
провайдера.
- Webhook-эндпоинт `/webhooks/yookassa` с проверкой подписи.
- Ежедневный job сверяет балансы с провайдером.
---
## 10. Авторизация
- Email + password (argon2id) — базовый сценарий.
- OAuth Yandex / VK / Google — для удобства.
- MFA TOTP — обязательно для админов и pro-игроков.
- JWT: HS256 в PoC, RS256 в проде.
- Refresh-token rotation, device-binding.
---
## 11. Anti-cheat
Уровни:
1. **Sanity** — диапазоны, скорости, ramp limits (всегда).
2. **CV-check** — расхождение с overhead-камерой > 1 м -> пометка.
3. **Replay verify** — после заезда можно пересчитать (асинхронно).
4. **Behavioral** — паттерны (слишком "идеальные" круги = подозрительно).
5. **Manual review** — оператор смотрит replay при споре.
---
## 12. Observability
- Логи: `slog` (stdlib) в JSON, отправка в Loki.
- Метрики: `prometheus/client_golang`, экспорт `/metrics`.
- Трейсы: OpenTelemetry, OTLP в Jaeger/Tempo.
- Ключевые метрики:
- `ws_connections_total`
- `rtt_ms_p50/p95/p99` (по `(clientSendTs - serverRecvTs)`)
- `tick_lag_ms` (если тик опаздывает > 5 мс — алерт).
- `input_rate_per_client`
- `dropped_snapshots_per_client`
- `gateway_online{trackId}` (heartbeat агенты в помещении).
---
## 13. Деплой и инфраструктура
**Базовое решение: PoC и MVP крутятся на Orange Pi 5 Pro через Coolify.**
Перенос на облако/VPS — когда появится коммерческая нагрузка и SLA обязательства.
### 13.1. Orange Pi 5 Pro (PoC и MVP)
**Платформа: Orange Pi 5 Pro** (Rockchip RK3588S, ARM64). Уже есть в наличии.
**Характеристики:**
- CPU: 8 cores (4× Cortex-A76 @ 2.4 GHz + 4× Cortex-A55 @ 1.8 GHz)
- RAM: 4 / 8 / 16 / 32 GB LPDDR4x (зависит от версии)
- GPU: Mali-G610 MP4
- **NPU: 6 TOPS** (RKNN) — для CV-инференса (YOLO/ByteTrack) **на борту**
- Сеть: 2× 2.5 GbE (один WAN, один LAN с трассой — **физическая изоляция**)
- Wi-Fi 6 (резерв)
- USB 3.0 × 2, PCIe 2.1 x1, SATA
- ОС: Ubuntu (уже стоит, с **Coolify**)
**Coolify как платформа деплоя:**
- Self-hosted PaaS, уже развёрнут.
- UI для управления сервисами, доменами, SSL, env-vars.
- Push-to-deploy из Git-репо (GitHub/Gitea).
- Сервисы = Docker Compose под капотом, но с UI и без боли.
- Встроенные: PostgreSQL, Redis, MinIO (S3-совместимое), Plausible, Uptime Kuma.
- Версионирование деплоев, rollback в 1 клик.
**Доступ из интернета (статический IP уже есть):**
- **Основной вариант**: Caddy reverse proxy + Let's Encrypt напрямую.
Coolify умеет провизить LE-сертификаты из коробки.
- Подходит, т.к. провайдер даёт статический IP, 443/tcp открыт на роутере
через port-forward на Orange Pi.
- Cloudflare Tunnel не нужен (нет смысла добавлять лишний hop).
- WAF: Crowdsec через Coolify (опц., можно отложить).
**Архитектура:**
```
+---------------- ORANGE PI 5 PRO (Ubuntu + Coolify) ----------------+
| |
| Coolify (docker compose orchestration) |
| +-- api-gateway ------------------------------------+ |
| +-- race-engine ----------------------------------+ |
| +-- postgres+timescale (через Coolify UI) | |
| +-- redis -----------------------------------+ |
| +-- caddy (LE) -----------------------------------+ |
| +-- prometheus+grafana -----------------------------------+ |
| +-- minio (S3 replays) -----------------------------------+ |
| |
| Natively (systemd) - требуют прямой доступ к железу: |
| +-- agent-linker (Wi-Fi к машинкам) |
| +-- cv-tracker (USB камеры, NPU RKNN) |
| +-- node_exporter (мониторинг) |
| |
+---------------------------------------------------------------------+
2.5 GbE #1 (WAN, к роутеру) 2.5 GbE #2 (LAN к трассе)
| |
vv
+--------------+ +---------------------+
| Internet | | Switch (или точка- |
| (clients) | | точка) |
+--------------+ +----------+----------+
|
+------v------+
| x0gp-track |
| AP + камеры |
+-------------+
```
**Изоляция сети трассы:**
- LAN-порт Orange Pi → выделенный коммутатор (или напрямую к точке доступа
x0gp-track).
- В этой сети только машинки и камеры — никаких домашних устройств.
- AP настраивается на изолированный SSID / VLAN.
- Это даёт предсказуемый latency (нет соседского трафика) и безопасность.
**Использование NPU RK3588S:**
- RKNN Toolkit 2 для конвертации моделей (PyTorch → ONNX → RKNN).
- Поддерживаются YOLOv8/v9, ByteTrack через onnxruntime-rknn.
- Целевой инференс: 60-120 FPS на одной камере 1080p (с запасом).
- Если NPU не подходит — fallback на CPU ARM A76.
- Альтернативный inference-сервер: Ultralytics + onnxruntime-rknn.
**ARM64-специфика:**
- Docker-образы должны быть `linux/arm64` или `multi-arch`.
- Postgres+TimescaleDB, Redis, Caddy, Prometheus, Grafana — есть для arm64.
- Go-сервисы — `GOARCH=arm64` при сборке (по умолчанию если собирать на
Orange Pi).
- CV-инференс на Python — pin версии onnxruntime-rknn под aarch64.
**Бэкапы:**
- Ежедневный `pg_dump` (cron в Coolify или отдельный scheduled task).
- Backup в MinIO (S3-совместимое) внутри Coolify.
- Опционально off-site: rclone → Backblaze B2 / Yandex Object Storage.
- Retention: 14 дней локально, 30 дней off-site.
**Безопасность:**
- SSH: только ключи, fail2ban.
- Coolify UI: за reverse proxy с basic auth или Cloudflare Access.
- Порты наружу: 80/tcp + 443/tcp (для Caddy/LE).
- Postgres и Redis слушают только 127.0.0.1 внутри Coolify.
- UPS для Orange Pi (microSD карты не любят внезапные отключения).
### 13.2. Production (после MVP, когда появится коммерческая нагрузка)
- VPS в Москве и СПб (2 региона) — Selectel / Yandex Cloud / Timeweb.
- Managed Postgres + Redis (для снятия с Orange Pi задач бэкапа и HA).
- Kubernetes (Yandex Cloud / Selectel / AWS) — HPA по метрикам
ws_connections, tick_lag.
- Redis Cluster, Postgres с read-replica.
- CDN для статики (CloudFront / Cloudflare).
- WAF (CrowdSec).
- Orange Pi остаётся как **gateway-агент + CV-инференс** в помещении с
трассой (NPU продолжаем использовать для позиционирования).
- Edge-сервер (Go) переезжает на VPS, к которому подключаются несколько
Orange Pi из разных помещений.
---
## 14. Производительность и SLO
| Метрика | Цель |
|---|---|
| API p99 latency | < 100 мс |
| WS input->ack | < 30 мс на Edge |
| Tick stability | 60 Hz +/- 2 мс |
| Один race-engine | 100 активных гонок |
| Один api-gateway | 5000 WS-клиентов |
| Доступность | 99.5% (PoC), 99.9% (prod) |
---
## 15. Roadmap (server)
| Фаза | Срок | Что делаем |
|---|---|---|
| **S0: PoC моно-сервис** | 1-2 нед | Один Go-бинарь: WS + REST + race state in-memory + локальный agent-linker. SQLite. Без биллинга. |
| **S1: MVP модульный** | 4-6 нед | Разделение на api-gateway / race-engine / agent-linker. Postgres + Redis. Auth, JWT, лобби. |
| **S2: CV + телеметрия** | 8-10 нед | Интеграция с overhead-CV. TimescaleDB. Replay recorder (S3-совместимое). |
| **S3: Биллинг** | 10-12 нед | ЮKassa, кошелёк, сессии под оплатой. Webhooks. Админка. |
| **S4: Турниры** | 14-18 нед | Bracket, лэддер, призовые. Анти-чит уровня 2-3. |
| **S5: Продакшн** | 4-6 мес | K8s, мульти-регион, SLO, on-call. Stripe для остального мира. Мобильное приложение. |
---
## 16. Открытые вопросы
- **Стек frontend**: React + Vite, какие библиотеки для WS (reconnect,
backoff)? Кандидаты: `reconnecting-websocket`, `socket.io-client` (overkill,
но быстро), свой тонкий клиент на `WebSocket` API.
- **Cloudflare Tunnel vs WireGuard vs статический IP** — что выберем для
доступа к домашнему серверу? Дефолт: Cloudflare Tunnel (проще, есть
free-tier, базовый WAF). Уточним после тестов latency.
- **Бэкапы Postgres на NAS** — есть ли дома NAS или использовать внешнее
S3-совместимое (Backblaze B2 / Yandex Object Storage) для off-site бэкапов?
- **CDN/streaming для replay** — Mux / Cloudflare Stream / своё? Out of scope
для MVP, но в плане держим "порт".
- **Интеграция со стриминговыми платформами (Twitch)** — out of scope для
MVP, но в плане держим "порт".
- **Язык CV-сервиса**: Go (gocv) или Python (быстрее итерация, ONNX-рантайм)?
Дефолт: Python на PoC для скорости итераций, Go для prod-инференса
(меньше ресурсов, проще деплой).
- **Конкретные характеристики домашнего сервера** (CPU/RAM/диск/ОС) — нужно
для оценки, сколько одновременных гонок он потянет. В PLAN.md пока заложен
разумный минимум (4 CPU / 16 GB / 500 GB SSD).
- **ЮKassa-аккаунт** — есть ли уже ИП/ООО для приёма платежей, или
регистрировать при переходе на биллинг?