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.
This commit is contained in:
x0gp
2026-06-22 22:01:09 +04:00
commit 978d36c505
71 changed files with 23500 additions and 0 deletions
+502
View File
@@ -0,0 +1,502 @@
# 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-аккаунт** — есть ли уже ИП/ООО для приёма платежей, или
регистрировать при переходе на биллинг?