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
+106
View File
@@ -0,0 +1,106 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>x0gp · PoC client</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<header class="topbar">
<div class="brand">
<span class="logo">x0gp</span>
<span class="tag">PoC client</span>
</div>
<div class="conn" id="connState">disconnected</div>
<div class="hud-top" id="hudTop">
<span>tick: <b id="tickHz">--</b> Hz</span>
<span>rtt: <b id="rtt">--</b> ms</span>
<span>seq: <b id="seq">0</b></span>
</div>
</header>
<main class="layout">
<!-- Левая колонка: трасса -->
<section class="track-wrap">
<canvas id="track" width="640" height="420"></canvas>
<div class="legend">
<span><i class="dot me"></i> вы</span>
<span><i class="dot other"></i> другие</span>
<span><i class="dot ghost"></i> correction (если будет)</span>
</div>
</section>
<!-- Правая колонка: управление и телеметрия -->
<aside class="controls">
<div class="card">
<h3>Подключение</h3>
<label>WS URL
<input id="wsUrl" type="text" value="ws://localhost:8080/ws" />
</label>
<label>Ник / driver_id
<input id="driverId" type="text" placeholder="driver-1" value="driver-1" />
</label>
<label>Race ID
<input id="raceId" type="text" value="demo-race" />
</label>
<div class="btn-row">
<button id="btnConnect" class="btn primary">Connect</button>
<button id="btnDisconnect" class="btn" disabled>Disconnect</button>
</div>
<div class="btn-row">
<button id="btnJoin" class="btn" disabled>Join race</button>
<button id="btnLeave" class="btn" disabled>Leave race</button>
</div>
</div>
<div class="card">
<h3>Управление</h3>
<p class="hint">
W/S — газ/тормоз, A/D — руль, Space — ручник, R — сброс позиции (debug)
</p>
<div class="pedals">
<div class="bar">
<span>throttle</span>
<div class="bar-track"><div id="barThrottle" class="bar-fill green"></div></div>
<b id="lblThrottle">0.00</b>
</div>
<div class="bar">
<span>brake</span>
<div class="bar-track"><div id="barBrake" class="bar-fill red"></div></div>
<b id="lblBrake">0.00</b>
</div>
<div class="bar">
<span>steer</span>
<div class="bar-track"><div id="barSteer" class="bar-fill blue"></div></div>
<b id="lblSteer">0.00</b>
</div>
</div>
<div class="wheel">
<div class="wheel-disc" id="wheelDisc"></div>
</div>
</div>
<div class="card">
<h3>Телеметрия</h3>
<dl class="tel">
<dt>speed</dt><dd id="tSpeed">0.00 m/s</dd>
<dt>heading</dt><dd id="tHeading">0.0°</dd>
<dt>lap</dt><dd id="tLap">0</dd>
<dt>sector</dt><dd id="tSector">-</dd>
<dt>pos</dt><dd id="tPos">(0.00, 0.00)</dd>
<dt>server_time</dt><dd id="tTsm">--</dd>
<dt>cars in race</dt><dd id="tCars">0</dd>
</dl>
</div>
<div class="card log-card">
<h3>Лог</h3>
<pre id="log" class="log"></pre>
</div>
</aside>
</main>
<script src="main.js"></script>
</body>
</html>