mirror of
https://github.com/IS1DI/x0gp.git
synced 2026-07-16 20:47:55 +00:00
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.
200 lines
6.8 KiB
Go
200 lines
6.8 KiB
Go
// Package main contains the genseed utility. It is built into a standalone
|
|
// binary via `go build ./scripts/genseed` and is not part of the runtime.
|
|
//
|
|
// Usage:
|
|
//
|
|
// go run ./scripts/genseed seeds.json > 002_seed.sql
|
|
//
|
|
// It reads the JSON dump produced by `DUMP_SEEDS=1 go test ./internal/catalog`
|
|
// and emits ON CONFLICT-safe INSERTs for the catalog tables.
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type waypoint struct {
|
|
X float64 `json:"x"`
|
|
Y float64 `json:"y"`
|
|
HeadingRad float64 `json:"heading_rad"`
|
|
SpeedMs float64 `json:"speed_ms"`
|
|
Curvature float64 `json:"curvature"`
|
|
}
|
|
|
|
type bounds struct {
|
|
MinX float64 `json:"min_x"`
|
|
MinY float64 `json:"min_y"`
|
|
MaxX float64 `json:"max_x"`
|
|
MaxY float64 `json:"max_y"`
|
|
LengthM float64 `json:"length_m"`
|
|
WidthM float64 `json:"width_m"`
|
|
}
|
|
|
|
type track struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
AuthorID string `json:"author_id"`
|
|
Visibility string `json:"visibility"`
|
|
Scale int `json:"scale"`
|
|
LengthM float64 `json:"length_m"`
|
|
WidthM float64 `json:"width_m"`
|
|
LaneWidthM float64 `json:"lane_width_m"`
|
|
Bounds bounds `json:"bounds"`
|
|
Surface string `json:"surface"`
|
|
Tags []string `json:"tags"`
|
|
Centerline []waypoint `json:"centerline"`
|
|
BestLapMs int64 `json:"best_lap_ms"`
|
|
BestLapHolder string `json:"best_lap_holder"`
|
|
CreatedMs int64 `json:"created_ms"`
|
|
UpdatedMs int64 `json:"updated_ms"`
|
|
}
|
|
|
|
type chassis struct {
|
|
Model string `json:"model"`
|
|
Material string `json:"material"`
|
|
Printed bool `json:"printed"`
|
|
WheelbaseMm float64 `json:"wheelbase_mm"`
|
|
TrackMm float64 `json:"track_mm"`
|
|
}
|
|
|
|
type motor struct {
|
|
Kind string `json:"kind"`
|
|
Class string `json:"class"`
|
|
KV int `json:"kv"`
|
|
PowerW float64 `json:"power_w"`
|
|
}
|
|
|
|
type battery struct {
|
|
VoltageV float64 `json:"voltage_v"`
|
|
CapacityMah int `json:"capacity_mah"`
|
|
Cells int `json:"cells"`
|
|
Chemistry string `json:"chemistry"`
|
|
}
|
|
|
|
type car struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
OwnerID string `json:"owner_id"`
|
|
Visibility string `json:"visibility"`
|
|
Scale int `json:"scale"`
|
|
LengthMm float64 `json:"length_mm"`
|
|
WidthMm float64 `json:"width_mm"`
|
|
HeightMm float64 `json:"height_mm"`
|
|
WeightG float64 `json:"weight_g"`
|
|
Chassis chassis `json:"chassis"`
|
|
Motor motor `json:"motor"`
|
|
Battery battery `json:"battery"`
|
|
Drive string `json:"drive"`
|
|
TopSpeedMs float64 `json:"top_speed_ms"`
|
|
ColorHex string `json:"color_hex"`
|
|
AvatarURL string `json:"avatar_url"`
|
|
Active bool `json:"active"`
|
|
CreatedMs int64 `json:"created_ms"`
|
|
UpdatedMs int64 `json:"updated_ms"`
|
|
}
|
|
|
|
type dump struct {
|
|
Tracks []track `json:"tracks"`
|
|
Cars []car `json:"cars"`
|
|
}
|
|
|
|
func main() {
|
|
if len(os.Args) < 2 {
|
|
fmt.Fprintln(os.Stderr, "usage: genseed <seeds.json>")
|
|
os.Exit(2)
|
|
}
|
|
f, err := os.Open(os.Args[1])
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer f.Close()
|
|
var d dump
|
|
if err := json.NewDecoder(f).Decode(&d); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
var b strings.Builder
|
|
b.WriteString("-- 002_seed.sql — initial system catalog (tracks + cars).\n")
|
|
b.WriteString("-- Generated from internal/catalog/seeddefs via scripts/genseed.\n")
|
|
b.WriteString("-- Idempotent: safe to re-apply (ON CONFLICT DO NOTHING).\n\n")
|
|
|
|
for _, t := range d.Tracks {
|
|
fmt.Fprintf(&b, "INSERT INTO tracks (id, name, description, author_id, visibility, scale,\n")
|
|
fmt.Fprintf(&b, " length_m, width_m, lane_width_m, surface, best_lap_ms, best_lap_holder,\n")
|
|
fmt.Fprintf(&b, " bounds_min_x, bounds_min_y, bounds_max_x, bounds_max_y,\n")
|
|
fmt.Fprintf(&b, " created_ms, updated_ms)\n")
|
|
fmt.Fprintf(&b, "VALUES (%s, %s, %s, %s, %s, %d,\n",
|
|
sqlStr(t.ID), sqlStr(t.Name), sqlStr(t.Description), sqlStr(t.AuthorID),
|
|
sqlStr(t.Visibility), t.Scale)
|
|
fmt.Fprintf(&b, " %s, %s, %s, %s, %d, %s,\n",
|
|
sqlF(t.LengthM), sqlF(t.WidthM), sqlF(t.LaneWidthM), sqlStr(t.Surface),
|
|
t.BestLapMs, sqlStr(t.BestLapHolder))
|
|
fmt.Fprintf(&b, " %s, %s, %s, %s,\n",
|
|
sqlF(t.Bounds.MinX), sqlF(t.Bounds.MinY), sqlF(t.Bounds.MaxX), sqlF(t.Bounds.MaxY))
|
|
fmt.Fprintf(&b, " %d, %d)\n", t.CreatedMs, t.UpdatedMs)
|
|
b.WriteString("ON CONFLICT (id) DO NOTHING;\n\n")
|
|
|
|
for i, wp := range t.Centerline {
|
|
fmt.Fprintf(&b, "INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)\n")
|
|
fmt.Fprintf(&b, "VALUES (%s, %d, %s, %s, %s, %s, %s)\n",
|
|
sqlStr(t.ID), i,
|
|
sqlF(wp.X), sqlF(wp.Y), sqlF(wp.HeadingRad), sqlF(wp.SpeedMs), sqlF(wp.Curvature))
|
|
b.WriteString("ON CONFLICT (track_id, seq) DO NOTHING;\n")
|
|
}
|
|
b.WriteString("\n")
|
|
|
|
for _, tag := range t.Tags {
|
|
fmt.Fprintf(&b, "INSERT INTO track_tags (track_id, tag) VALUES (%s, %s)\n",
|
|
sqlStr(t.ID), sqlStr(tag))
|
|
b.WriteString("ON CONFLICT DO NOTHING;\n")
|
|
}
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
for _, c := range d.Cars {
|
|
fmt.Fprintf(&b, "INSERT INTO cars (id, name, owner_id, visibility, scale,\n")
|
|
fmt.Fprintf(&b, " length_mm, width_mm, height_mm, weight_g,\n")
|
|
fmt.Fprintf(&b, " chassis_model, chassis_material, chassis_printed, chassis_wheelbase_mm, chassis_track_mm,\n")
|
|
fmt.Fprintf(&b, " motor_kind, motor_class, motor_kv, motor_power_w,\n")
|
|
fmt.Fprintf(&b, " battery_voltage_v, battery_capacity_mah, battery_cells, battery_chemistry,\n")
|
|
fmt.Fprintf(&b, " drive, top_speed_ms, color_hex, avatar_url, active,\n")
|
|
fmt.Fprintf(&b, " created_ms, updated_ms)\n")
|
|
fmt.Fprintf(&b, "VALUES (%s, %s, %s, %s, %d,\n",
|
|
sqlStr(c.ID), sqlStr(c.Name), sqlStr(c.OwnerID), sqlStr(c.Visibility), c.Scale)
|
|
fmt.Fprintf(&b, " %s, %s, %s, %s,\n",
|
|
sqlF(c.LengthMm), sqlF(c.WidthMm), sqlF(c.HeightMm), sqlF(c.WeightG))
|
|
fmt.Fprintf(&b, " %s, %s, %s, %s, %s,\n",
|
|
sqlStr(c.Chassis.Model), sqlStr(c.Chassis.Material), sqlBool(c.Chassis.Printed),
|
|
sqlF(c.Chassis.WheelbaseMm), sqlF(c.Chassis.TrackMm))
|
|
fmt.Fprintf(&b, " %s, %s, %d, %s,\n",
|
|
sqlStr(c.Motor.Kind), sqlStr(c.Motor.Class), c.Motor.KV, sqlF(c.Motor.PowerW))
|
|
fmt.Fprintf(&b, " %s, %d, %d, %s,\n",
|
|
sqlF(c.Battery.VoltageV), c.Battery.CapacityMah, c.Battery.Cells, sqlStr(c.Battery.Chemistry))
|
|
fmt.Fprintf(&b, " %s, %s, %s, %s, %s,\n",
|
|
sqlStr(c.Drive), sqlF(c.TopSpeedMs), sqlStr(c.ColorHex), sqlStr(c.AvatarURL), sqlBool(c.Active))
|
|
fmt.Fprintf(&b, " %d, %d)\n", c.CreatedMs, c.UpdatedMs)
|
|
b.WriteString("ON CONFLICT (id) DO NOTHING;\n\n")
|
|
}
|
|
|
|
os.Stdout.WriteString(b.String())
|
|
}
|
|
|
|
func sqlStr(s string) string {
|
|
return "'" + strings.ReplaceAll(s, "'", "''") + "'"
|
|
}
|
|
|
|
func sqlBool(b bool) string {
|
|
if b {
|
|
return "TRUE"
|
|
}
|
|
return "FALSE"
|
|
}
|
|
|
|
func sqlF(f float64) string {
|
|
return strconv.FormatFloat(f, 'f', -1, 64)
|
|
} |