diff --git a/.gitignore b/.gitignore index 160fabb..be35af8 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ server/poc-server.exe .grepai/ *.blend1 *.blend2 +node_modules/ + diff --git a/AGENTS.md b/AGENTS.md index c8e8eb4..9a638f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,6 +43,17 @@ - Go 1.26, бинарь `cmd/poc-server`. Сборка: `go build ./...` или `go build -o poc-server.exe ./cmd/poc-server`. - HTTP+WebSocket, роутер `http.NewServeMux`. Запуск требует `DATABASE_URL` (см. `.env`). +- **База данных (Supabase)**: + - Используется self-hosted Supabase на порту `3402` (хост: `80.234.93.161:3402`). + - Расширение `timescaledb` отключено (закомментировано в `001_init.sql`), так как Supabase не поставляет его из коробки. + - Нативная папка `supabase/` содержит `config.toml`, объединенный дамп схемы `supabase/migrations/20260717000000_schema.sql` и каталог семян `supabase/seed.sql` (включает рескейл Монако). + - Объединенный дамп схемы автоматически заполняет таблицу `schema_migrations`, чтобы предотвратить ошибки повторного применения миграций Go-сервером на старте. +- **Абстракция данных (Store)**: + - Пакеты `clans` и `drivers` используют интерфейсы `Store` вместо прямого импорта `*PgStore`. + - Это позволяет писать юнит-тесты сервисов без подключения к реальной БД (через `memoryStore`). +- **Слепок структуры (Graphify)**: + - Утилита `scripts/graphify/main.go` собирает описание всех экспортируемых пакетов, структур, интерфейсов и функций в компактный файл [server/CODEBASE.md](file:///d:/x0gp/server/CODEBASE.md). + - Рекомендуется запускать `go run ./scripts/graphify/main.go` после значимых изменений API для поддержания карты проекта в актуальном состоянии и экономии токенов при чтении агентом. - **Swagger UI**: `GET /swagger/index.html`, спека `GET /swagger/doc.json` (14 paths, 29 definitions). - Генерация спеки: `swag init -g cmd/poc-server/main.go -o docs --parseInternal` из `D:/x0gp/server`. CLI ставится через `go install github.com/swaggo/swag/cmd/swag@v1.16.4`. - Аннотации swag живут над хендлерами в `cmd/poc-server/main.go`, `catalog_handlers.go`, `races_handlers.go`. При добавлении нового REST-эндпоинта — добавить `@Router`/`@Param`/`@Success` и перегенерить `docs/`. diff --git a/server/CODEBASE.md b/server/CODEBASE.md new file mode 100644 index 0000000..852887d --- /dev/null +++ b/server/CODEBASE.md @@ -0,0 +1,736 @@ +# x0gp Go Codebase Map (Generated) + +This file contains a compiled map of all packages, structures, interfaces, and public functions in the `server` directory. It is optimized for token-efficiency so AI agents and humans can understand the codebase layout without reading all files. + +## Package `config` (`internal/config`) + +Package config loads runtime configuration from environment variables. + +On startup Load() reads a .env file (if present) from the current +working directory and merges its values into the process environment. +Existing process env vars take precedence over .env so deployments +can override .env without modifying the file. + +### Structs +- **struct Config** { HTTPAddr, TickRate, TickInterval, LogLevel, MaxClients, SnapshotRate, DevMode, DatabaseURL, UDPVideoAddr, ESPCmdPort } + +### Functions +- func Load() (*Config, error) + +--- + +## Package `postgres` (`internal/storage/postgres`) + +Package postgres owns the persistence layer of the catalog. + +It exposes a *pgxpool.Pool plus a small set of repository helpers used by +internal/catalog. Migrations are applied at startup from the SQL files in +the migrations/ subdirectory. + +### Structs +- **struct PgStore** +- **struct Config** { URL, Host, Port, User, Password, Database, SSLMode, MaxConns, MinConns, ConnectTimeout } + +### Functions +- func NewPgStore(*pgxpool.Pool) *PgStore +- func Open(context.Context, Config) (*pgxpool.Pool, error) +- func Migrate(context.Context, *pgxpool.Pool) error + +### Methods +- func (*PgStore) Load(context.Context) ([]catalog.TrackMeta, []catalog.CarMeta, error) +- func (*PgStore) UpsertTrack(context.Context, catalog.TrackMeta) error +- func (*PgStore) DeleteTrack(context.Context, string) error +- func (*PgStore) UpsertCar(context.Context, catalog.CarMeta) error +- func (*PgStore) DeleteCar(context.Context, string) error +- func (*PgStore) UpdateCarStats(context.Context, string, func) error +- func (*PgStore) GetTrackCalendar(context.Context) ([]catalog.TrackCalendarEntry, error) +- func (*PgStore) CreateTrackCalendar(context.Context, catalog.TrackCalendarEntry) (catalog.TrackCalendarEntry, error) +- func (*PgStore) DeleteTrackCalendar(context.Context, int) error +- func (Config) DSN() (string, error) + +--- + +## Package `seed` (`internal/races/seed`) + +### Structs +- **struct DefaultCounts** { Finished, Live, Plans } +- **struct Options** { Counts, Seed, Reset, Now } +- **struct Summary** { FinishedInserted, LiveCreated, PlansInserted, QueueInserted, Duration } +- **struct Runner** + +### Functions +- func NewRunner(*races.PgStore, *lobby.Service, *races.LiveStore, *clans.PgStore, *drivers.PgStore) *Runner + +### Methods +- func (*Runner) Run(context.Context, Options) (Summary, error) + +--- + +## Package `realtime` (`internal/realtime`) + +Package realtime manages WebSocket connections: registration, fan-out of +race snapshots, and per-client send queues with backpressure handling. + +### Structs +- **struct Client** { ID, Send, SessionID, Done, DriverID, LastUDPCommand } +- **struct Hub** +- **struct Stats** { Connections, DropsTotal } + +### Functions +- func NewHub() *Hub +- func MustEnvelope(transport.MessageType, any) *transport.Envelope + +### Methods +- func (*Client) Close() +- func (*Hub) Stats() Stats +- func (*Hub) Register(*Client) +- func (*Hub) Unregister(*Client) +- func (*Hub) Run(context.Context) +- func (*Hub) Stop() +- func (*Hub) Publish(*transport.Envelope) + +--- + +## Package `catalog` (`internal/catalog`) + +Package catalog owns the static configuration entities: tracks and +cars. Both are first-class resources that drivers browse, pick, and +(in later phases) author. + +PoC: in-memory cache backed by a pluggable Store. The runtime uses +postgres.PgStore so tracks / cars survive restarts. The cache is the +source of truth for read paths; the Store is the source of truth for +durability. Mutations go through the Store, then refresh the cache. + +Design constraints come from the user's apartment layout: + - largest room: 4.46 m × 3.19 m (446 × 319 cm) + - car scale: 1/27 (Compact) + - printer: Bambu A1 (220×220×250 mm build volume) + +Tracks are sized to fit a 4.5 × 3.2 m bounding box with a safety +margin, which keeps them physically realizable in the apartment. +Cars are sized to a 1/27 touring envelope (~80 × 35 × 25 mm). + +### Interfaces +- **interface Store** { Load, UpsertTrack, DeleteTrack, UpsertCar, DeleteCar, UpdateCarStats, GetTrackCalendar, CreateTrackCalendar, DeleteTrackCalendar } + +### Structs +- **struct CreateTrackOptions** { ID, Name, Description, AuthorID, Visibility, Scale, LengthM, WidthM, LaneWidthM, Surface, Tags, Centerline } +- **struct UpdateTrackOptions** { Name, Description, Visibility, LengthM, WidthM, LaneWidthM, Surface, Tags, Centerline } +- **struct CreateCarOptions** { ID, Name, OwnerID, Visibility, Scale, LengthMm, WidthMm, HeightMm, WeightG, Chassis, Motor, Battery, Drive, TopSpeedMs, ColorHex, AvatarURL, Active, DeviceID } +- **struct UpdateCarOptions** { Name, Visibility, LengthMm, WidthMm, HeightMm, WeightG, TopSpeedMs, ColorHex, Active, Chassis, Motor, Battery, Drive, DeviceID } +- **struct Waypoint** { X, Y, HeadingRad, SpeedMs, Curvature } +- **struct Bounds** { MinX, MinY, MaxX, MaxY, LengthM, WidthM } +- **struct TrackMeta** { ID, Name, Description, AuthorID, Visibility, Scale, LengthM, WidthM, LaneWidthM, Bounds, Surface, Tags, Centerline, BestLapMs, BestLapHolder, CreatedMs, UpdatedMs } +- **struct TrackCalendarEntry** { ID, TrackID, StartAt, EndAt } +- **struct MotorSpec** { Kind, Class, KV, PowerW } +- **struct BatterySpec** { VoltageV, CapacityMah, Cells, Chemistry } +- **struct ChassisSpec** { Model, Material, Printed, WheelbaseMm, TrackMm } +- **struct CarMeta** { ID, Name, OwnerID, Visibility, Scale, LengthMm, WidthMm, HeightMm, WeightG, Chassis, Motor, Battery, Drive, DeviceID, TopSpeedMs, ColorHex, AvatarURL, Active, TotalDistanceM, TotalRaces, TotalLaps, BestLapMs, CreatedMs, UpdatedMs } +- **struct Snapshot** { GeneratedMs, Version, Tracks, Cars } +- **struct Stats** { TracksTotal, TracksSystem, TracksPublic, TracksPrivate, CarsTotal, CarsSystem, CarsPublic, CarsPrivate, CarsActive } +- **struct Event** { Kind, TrackID, CarID, Snapshot } +- **struct Service** + +### Functions +- func NewService(context.Context, Store) (*Service, error) + +### Methods +- func (CreateTrackOptions) Validate() error +- func (*Service) CreateTrack(context.Context, CreateTrackOptions) (TrackMeta, error) +- func (*Service) UpdateTrack(context.Context, string, UpdateTrackOptions) (TrackMeta, error) +- func (*Service) DeleteTrack(context.Context, string) error +- func (CreateCarOptions) Validate() error +- func (*Service) CreateCar(context.Context, CreateCarOptions) (CarMeta, error) +- func (*Service) UpdateCar(context.Context, string, UpdateCarOptions) (CarMeta, error) +- func (*Service) DeleteCar(context.Context, string) error +- func (*Service) SetCarStats(context.Context, string, func) error +- func (*Service) ListTracksByVisibility(Visibility) []TrackMeta +- func (*Service) ListCarsByVisibility(Visibility) []CarMeta +- func (*Service) ListCarsByOwner(string) []CarMeta +- func (*Service) GetTrackCalendar(context.Context) ([]TrackCalendarEntry, error) +- func (*Service) CreateTrackCalendar(context.Context, string, time.Time, time.Time) (TrackCalendarEntry, error) +- func (*Service) DeleteTrackCalendar(context.Context, int) error +- func (*memoryStore) Load(context.Context) ([]TrackMeta, []CarMeta, error) +- func (*memoryStore) UpsertTrack(context.Context, TrackMeta) error +- func (*memoryStore) DeleteTrack(context.Context, string) error +- func (*memoryStore) UpsertCar(context.Context, CarMeta) error +- func (*memoryStore) DeleteCar(context.Context, string) error +- func (*memoryStore) UpdateCarStats(context.Context, string, func) error +- func (*memoryStore) GetTrackCalendar(context.Context) ([]TrackCalendarEntry, error) +- func (*memoryStore) CreateTrackCalendar(context.Context, TrackCalendarEntry) (TrackCalendarEntry, error) +- func (*memoryStore) DeleteTrackCalendar(context.Context, int) error +- func (*Service) Subscribe() (?, func) +- func (*Service) Stop() +- func (*Service) Done() ? +- func (*Service) Snapshot() Snapshot +- func (*Service) Stats() Stats +- func (*Service) ListTracks() []TrackMeta +- func (*Service) ListCars() []CarMeta +- func (*Service) GetTrack(string) (TrackMeta, bool) +- func (*Service) GetCar(string) (CarMeta, bool) + +--- + +## Package `seeddefs` (`internal/catalog/seeddefs`) + +Package seeddefs holds the canonical seed definitions for tracks and +cars. It lives in its own package (no postgres dependencies) so both +the catalog tests and the scripts/genseed SQL generator can consume +the same source-of-truth data. + +### Structs +- **struct Waypoint** { X, Y, HeadingRad, SpeedMs, Curvature } +- **struct Bounds** { MinX, MinY, MaxX, MaxY, LengthM, WidthM } +- **struct Track** { ID, Name, Description, AuthorID, Visibility, Scale, LengthM, WidthM, LaneWidthM, Surface, Tags, Centerline, Bounds, BestLapMs, BestLapHolder } +- **struct Chassis** { Model, Material, Printed, WheelbaseMm, TrackMm } +- **struct Motor** { Kind, Class, KV, PowerW } +- **struct Battery** { VoltageV, CapacityMah, Cells, Chemistry } +- **struct Car** { ID, Name, OwnerID, Visibility, Scale, LengthMm, WidthMm, HeightMm, WeightG, Chassis, Motor, Battery, Drive, TopSpeedMs, ColorHex, Active, TotalDistanceM, TotalRaces, TotalLaps, BestLapMs } + +### Functions +- func DefaultTracks() []Track +- func DefaultCars() []Car +- func Monaco() Track +- func BarcelonaCatalunya() Track +- func AustriaRedBullRing() Track +- func GreatBritainSilverstone() Track +- func BelgiumSpa() Track +- func RedBullRB20() Car +- func FerrariSF24() Car +- func McLarenMCL38() Car +- func MercedesW15() Car +- func AstonMartinAMR24() Car +- func ComputeBounds([]Waypoint) Bounds + +--- + +## Package `clans` (`internal/clans`) + +Package clans owns persistence and business logic for clans. + +A clan has a unique 3-letter tag (A-Z, uppercase, exactly 3 chars), +a name, and an optional avatar URL. Drivers reference a clan via a +nullable FK (see internal/drivers). + +### Interfaces +- **interface Store** { Exec, Create, Get, GetByTag, List, Delete, Update, Count } + +### Structs +- **struct Service** +- **struct CreateInput** { ID, Tag, Name, AvatarURL } +- **struct UpdateInput** { Name, AvatarURL } +- **struct Clan** { ID, Tag, Name, AvatarURL, CreatedMs, UpdatedMs } +- **struct PgStore** + +### Functions +- func NewService(Store) *Service +- func IsValidTag(string) bool +- func NewPgStore(*pgxpool.Pool) *PgStore + +### Methods +- func (*Service) Create(context.Context, CreateInput) (Clan, error) +- func (*Service) Get(context.Context, string) (Clan, error) +- func (*Service) GetByTag(context.Context, string) (Clan, error) +- func (*Service) List(context.Context, int, int) ([]Clan, error) +- func (*Service) Update(context.Context, string, UpdateInput) (Clan, error) +- func (*Service) Delete(context.Context, string) error +- func (*PgStore) Exec(context.Context, string) (int64, error) +- func (*PgStore) Create(context.Context, Clan) error +- func (*PgStore) Get(context.Context, string) (Clan, error) +- func (*PgStore) GetByTag(context.Context, string) (Clan, error) +- func (*PgStore) List(context.Context, int, int) ([]Clan, error) +- func (*PgStore) Delete(context.Context, string) error +- func (*PgStore) Update(context.Context, Clan) error +- func (*PgStore) Count(context.Context) (int, error) + +--- + +## Package `control` (`internal/control`) + +Package control owns the authoritative race state. + +For PoC, the physics model is intentionally simple (no tire slip, no +collision): each car integrates position from (throttle, brake, steering). +Realistic physics, anti-cheat checks, and CV-based ground truth will be +added in MVP / production phases. + +### Structs +- **struct Car** { ID, DriverID, DriverName, DeviceID, X, Y, Heading, Speed, Lap, Sector, LastLapMs, BestLapMs, DNF, AppliedSteering, AppliedThrottle, AppliedBrake } +- **struct Engine** + +### Functions +- func NewEngine(int) *Engine + +### Methods +- func (RacePhase) String() string +- func (*Car) ApplyInput(transport.InputState, float64) +- func (*Car) Snapshot() transport.CarInfo +- func (*Engine) Snapshots() ? +- func (*Engine) Phase() RacePhase +- func (*Engine) SetPhase(RacePhase) +- func (*Engine) AddCar(string, string, int, *int) (*Car, error) +- func (*Engine) RemoveCar(string) +- func (*Engine) GetCarByDriver(string) *Car +- func (*Engine) Run(context.Context) +- func (*Engine) Stop() + +--- + +## Package `drivers` (`internal/drivers`) + +Package drivers owns persistence and business logic for drivers +(racers). Each driver has a unique 3-letter nickname (A-Z, +uppercase, exactly 3 chars), a name, an optional avatar URL and an +optional clan reference. + +### Interfaces +- **interface Store** { Exec, Create, Get, GetByNickname, List, Delete, Update, Count } + +### Structs +- **struct Service** +- **struct CreateInput** { ID, Nickname, Name, AvatarURL, ClanID } +- **struct UpdateInput** { Name, AvatarURL, ClanID } +- **struct Driver** { ID, Nickname, Name, AvatarURL, ClanID, CreatedMs, UpdatedMs } +- **struct PgStore** + +### Functions +- func NewService(Store) *Service +- func IsValidNickname(string) bool +- func NewPgStore(*pgxpool.Pool) *PgStore + +### Methods +- func (*Service) Create(context.Context, CreateInput) (Driver, error) +- func (*Service) Get(context.Context, string) (Driver, error) +- func (*Service) GetByNickname(context.Context, string) (Driver, error) +- func (*Service) List(context.Context, int, int, string) ([]Driver, error) +- func (*Service) Update(context.Context, string, UpdateInput) (Driver, error) +- func (*Service) Delete(context.Context, string) error +- func (*PgStore) Exec(context.Context, string) (int64, error) +- func (*PgStore) Create(context.Context, Driver) error +- func (*PgStore) Get(context.Context, string) (Driver, error) +- func (*PgStore) GetByNickname(context.Context, string) (Driver, error) +- func (*PgStore) List(context.Context, int, int, string) ([]Driver, error) +- func (*PgStore) Update(context.Context, Driver) error +- func (*PgStore) Delete(context.Context, string) error +- func (*PgStore) Count(context.Context) (int, error) + +--- + +## Package `lobby` (`internal/lobby`) + +Package lobby owns the "outside any race" state of the server: which +races are open, which drivers are connected but idle, who is the host +of each race. + +The lobby is the single source of truth for matchmaking and for +rendering the "what races are available right now" list. It does NOT +own per-race physics — that's the Engine (internal/control). + +All operations are thread-safe. Mutations broadcast a snapshot event +to subscribers (see Service.Subscribe). + +### Interfaces +- **interface Persistence** { UpsertRace, DeleteRace, AddDriver, RemoveDriver, SetRaceStatus, UpsertDriver, DeleteDriver } + +### Structs +- **struct CreateRaceOptions** { ID, Name, TrackID, MaxCars, Laps, TimeLimitS } +- **struct RaceMeta** { ID, Name, TrackID, MaxCars, Laps, TimeLimitS, DriverIDs, Status, CreatedMs, StartedMs } +- **struct DriverMeta** { ID, Name, Nickname, AvatarURL, ClanID, ClanTag, Status, RaceID, ConnectedMs, LastSeenMs, Country, AvatarHue, CarID } +- **struct Snapshot** { GeneratedMs, Races, Drivers, Version } +- **struct Stats** { RacesTotal, RacesOpen, RacesActive, DriversIdle, DriversTotal } +- **struct Service** + +### Functions +- func NewService() *Service + +### Methods +- func (*Service) CreateRace(CreateRaceOptions) (RaceMeta, error) +- func (*Service) AddRace(RaceMeta) +- func (*Service) AddDriver(string, string, string) (DriverMeta, error) +- func (*Service) RemoveDriver(string) +- func (*Service) Heartbeat(string) +- func (*Service) SetDriverProfile(string, string, string, string, string) +- func (*Service) AddDriverToRace(string, string) error +- func (*Service) RemoveDriverFromRace(string) +- func (*Service) SetRaceStatus(string, RaceStatus) error +- func (*Service) DeleteRace(string) error +- func (*Service) QuickPlay(string, int) (RaceMeta, error) +- func (*Service) GetRace(string) (RaceMeta, error) +- func (*Service) GetDriver(string) (DriverMeta, error) +- func (*Service) SelectCar(string, *string) error +- func (*Service) ListRaces() []RaceMeta +- func (*Service) ListDrivers() []DriverMeta +- func (*Service) SetPersistence(Persistence) +- func (*Service) SetRaceLifecycleHook(func, func) +- func (*Service) Subscribe() (?, func) +- func (*Service) Stop() +- func (*Service) Done() ? +- func (*Service) Snapshot() Snapshot +- func (*Service) Stats() Stats + +--- + +## Package `stats` (`internal/stats`) + +Package stats collects and exposes server and per-driver metrics. + +PoC: in-memory only, atomic counters where possible, mutex around +driver and race records. The data lives for the lifetime of the +process. A future storage layer will swap this for Postgres/Redis. + +Public surface is split into: + - lifecycle hooks (Connect/Disconnect/Race*/Lap*/Input*/RTT) — + called from cmd/poc-server and from internal/control and lobby + - Snapshot() — full server snapshot for /stats/detailed + - DriverProfile(id) — per-driver summary + - RecentLaps(n) — last N laps across all races + +### Structs +- **struct ServerStats** { StartedMs, UptimeMs, CurrentClients, PeakClients, TotalConnections, TotalDisconnects, TotalRacesCreated, TotalRacesStarted, TotalRacesFinished, TotalLapsRecorded, TotalInputsRecv, TotalSnapshotsSent, SnapshotsDropped, AvgRTTMs, RecentRTTs } +- **struct DriverProfile** { ID, Name, TotalRacesStarted, TotalRacesFinished, TotalLaps, BestLapMs, LastLapMs, TotalDistanceM, TotalPlaytimeMs, Wins, Podiums, LastSeenMs } +- **struct LapRecord** { RaceID, DriverID, LapMs, Sector1Ms, Sector2Ms, Sector3Ms, Position, TSMs } +- **struct RaceResult** { RaceID, DriverID, FinishedAtMs, Position, TotalLaps, BestLapMs, TotalTimeMs, DNF } +- **struct Collector** + +### Functions +- func NewCollector() *Collector + +### Methods +- func (*Collector) Start() +- func (*Collector) OnConnect(string, string) +- func (*Collector) OnDisconnect(string, int64) +- func (*Collector) OnRaceCreated() +- func (*Collector) OnRaceStarted([]string) +- func (*Collector) OnRaceFinished(RaceResult) +- func (*Collector) OnLapRecorded(LapRecord) +- func (*Collector) OnInputProcessed() +- func (*Collector) OnSnapshotSent(uint64, uint64) +- func (*Collector) OnRTT(int64) +- func (*Collector) OnDistance(string, float64) +- func (*Collector) Server() ServerStats +- func (*Collector) Driver(string, string) DriverProfile +- func (*Collector) Drivers() []DriverProfile +- func (*Collector) RecentLaps(int) []LapRecord +- func (*Collector) RecentResults(int) []RaceResult + +--- + +## Package `races` (`internal/races`) + +Package races owns the cross-cutting "race list" surface: keyset-paginated +listing of live and finished races, upcoming (next-N) selection, the +driver-to-race queue and the recurring race-plan scheduler. + +The package composes two storage backends: + + - lobby.Service (in-memory): authoritative source for active races + (status = lobby | countdown | racing). Used directly for the live + page of /api/races, for the upcoming view and for the scheduler + materialisation. + - Postgres (pgxpool): authoritative for finished races (snapshotted + on SetRaceStatus(finished)), for race plans and for the queue. + Finished races are read with keyset pagination on + (finished_ms DESC, id DESC). + +### Structs +- **struct Cursor** { Ms, ID } +- **struct LeaderboardEntry** { DriverID, Name, Nickname, AvatarURL, ClanID, ClanTag, Points, BestPos, BestTimeMs, Rank } +- **struct LeaderboardResult** { Items, Total, CurrentDriver } +- **struct TrackCalendarInfo** { ID, TrackID, TrackName, StartAt, EndAt } +- **struct TrackCalendarLeaderboard** { TrackID, TrackName, StartAt, EndAt, Status, Podium, CurrentDriver } +- **struct LiveStore** +- **struct Service** +- **struct ListFilter** { Statuses, TrackID, Cursor, Limit } +- **struct RaceItem** { Source, Meta, Podium } +- **struct ListResult** { Items, NextCursor, TotalCount } +- **struct UpcomingItem** { Meta, QueueLen, PlanID, StartAtMs } +- **struct JoinUpcomingResult** { Joined, Skipped } +- **struct SnapshotFinished** { Meta, FinishedMs, TotalLaps, WinnerDriverID, WinnerName, BestLapMs, Results } +- **struct CreatePlanInput** { ID, Name, TrackID, MaxCars, Laps, TimeLimitS, StartAtMs, IntervalS, Count, Enabled } +- **struct Scheduler** +- **struct PodiumEntry** { Position, DriverID, Name, TotalTimeMs, Nickname, AvatarUrl, ClanID } +- **struct DriverResult** { DriverID, Name, TotalTimeMs, BestLapMs, Position, Nickname, AvatarUrl, ClanID } +- **struct FinishedRace** { ID, Name, TrackID, MaxCars, Laps, TimeLimitS, DriverIDs, Status, CreatedMs, StartedMs, FinishedMs, DurationMs, TotalLaps, TotalDrivers, Results, WinnerDriverID, WinnerName, BestLapMs, Podium } +- **struct RacePlan** { ID, Name, TrackID, MaxCars, Laps, TimeLimitS, StartAtMs, IntervalS, Count, Enabled, CreatedMs, UpdatedMs, NextFireMs, FiresDone } +- **struct QueueEntry** { DriverID, RaceID, PlanID, EnqueuedMs } +- **struct PgStore** + +### Functions +- func DecodeCursor(string) (Cursor, error) +- func NewLiveStore(*PgStore) *LiveStore +- func NewService(*PgStore, *lobby.Service) *Service +- func NewScheduler(*PgStore, *lobby.Service, time.Duration) *Scheduler +- func NewPgStore(*pgxpool.Pool) *PgStore +- func ParseStatusFilter(string) ([]StatusFilter, error) + +### Methods +- func (Cursor) String() string +- func (Cursor) IsZero() bool +- func (Cursor) Encode() string +- func (*PgStore) GetTrackLeaderboard(context.Context, string, int, int, int) ([]LeaderboardEntry, int, error) +- func (*PgStore) GetTrackLeaderboardDriver(context.Context, string, int, string) (*LeaderboardEntry, error) +- func (*PgStore) GetOverallLeaderboard(context.Context, int, int, int) ([]LeaderboardEntry, int, error) +- func (*PgStore) GetOverallLeaderboardDriver(context.Context, int, string) (*LeaderboardEntry, error) +- func (*PgStore) GetDriverLeaderboardProfile(context.Context, string) (*LeaderboardEntry, error) +- func (*PgStore) GetCalendarLeaderboardTracks(context.Context) ([]TrackCalendarInfo, error) +- func (*LiveStore) UpsertRace(context.Context, lobby.RaceMeta) error +- func (*LiveStore) DeleteRace(context.Context, string) error +- func (*LiveStore) AddDriver(context.Context, string, string, int) error +- func (*LiveStore) RemoveDriver(context.Context, string, string) error +- func (*LiveStore) SetRaceStatus(context.Context, string, lobby.RaceStatus, int64) error +- func (*LiveStore) UpsertDriver(context.Context, lobby.DriverMeta) error +- func (*LiveStore) DeleteDriver(context.Context, string) error +- func (*LiveStore) ListAllRaces(context.Context) ([]lobby.RaceMeta, error) +- func (*LiveStore) ListAllDrivers(context.Context) ([]lobby.DriverMeta, error) +- func (*LiveStore) ListLivePaged(context.Context, []string, string, Cursor, int) ([]lobby.RaceMeta, error) +- func (*LiveStore) ListUpcoming(context.Context, int) ([]lobby.RaceMeta, error) +- func (*LiveStore) Exec(context.Context, string) (int64, error) +- func (*Service) SetLiveStore(*LiveStore) +- func (*Service) RestoreFromDB(context.Context) (int, int, error) +- func (*Service) SetMaxUpcoming(int) +- func (*Service) ListRaces(context.Context, ListFilter) (ListResult, error) +- func (*Service) Upcoming(context.Context, int) ([]UpcomingItem, error) +- func (*Service) JoinUpcoming(context.Context, string, int) (JoinUpcomingResult, error) +- func (*Service) LeaveQueue(context.Context, string, string) error +- func (*Service) ListQueue(context.Context, string) ([]QueueEntry, error) +- func (*Service) PersistFinished(context.Context, SnapshotFinished) error +- func (*Service) CreatePlan(context.Context, CreatePlanInput) (RacePlan, error) +- func (*Service) ListPlans(context.Context, Cursor, int) ([]RacePlan, error) +- func (*Service) CountPlans(context.Context) (int, error) +- func (*Service) DeletePlan(context.Context, string) error +- func (*Service) GetLeaderboard(context.Context, string, int, int, int, string) (*LeaderboardResult, error) +- func (*Service) GetCalendarLeaderboard(context.Context, int, string) ([]TrackCalendarLeaderboard, error) +- func (*Scheduler) Run(context.Context) +- func (*Scheduler) Stop() +- func (*Scheduler) Done() ? +- func (*PgStore) Exec(context.Context, string, ?) (int64, error) +- func (*PgStore) ListTrackIDs(context.Context) ([]string, error) +- func (*PgStore) UpsertLive(context.Context, lobby.RaceMeta) error +- func (*PgStore) DeleteLive(context.Context, string) error +- func (*PgStore) AddLiveDriver(context.Context, string, string, int) error +- func (*PgStore) RemoveLiveDriver(context.Context, string, string) error +- func (*PgStore) SetLiveStatus(context.Context, string, lobby.RaceStatus, int64) error +- func (*PgStore) ListLivePaged(context.Context, []string, string, Cursor, int) ([]lobby.RaceMeta, error) +- func (*PgStore) ListLiveUpcoming(context.Context, int) ([]lobby.RaceMeta, error) +- func (*PgStore) GetLive(context.Context, string) (lobby.RaceMeta, error) +- func (*PgStore) ListAllRaces(context.Context) ([]lobby.RaceMeta, error) +- func (*PgStore) UpsertLobbyDriver(context.Context, lobby.DriverMeta) error +- func (*PgStore) DeleteLobbyDriver(context.Context, string) error +- func (*PgStore) ListLobbyDrivers(context.Context) ([]lobby.DriverMeta, error) +- func (*PgStore) InsertFinished(context.Context, FinishedRace) error +- func (*PgStore) ListFinished(context.Context, string, Cursor, int) ([]FinishedRace, error) +- func (*PgStore) CountRaces(context.Context, []string, string) (int, error) +- func (*PgStore) CreatePlan(context.Context, RacePlan) error +- func (*PgStore) GetPlan(context.Context, string) (RacePlan, error) +- func (*PgStore) ListPlans(context.Context, Cursor, int) ([]RacePlan, error) +- func (*PgStore) CountPlans(context.Context) (int, error) +- func (*PgStore) DeletePlan(context.Context, string) error +- func (*PgStore) SetPlanEnabled(context.Context, string, bool) error +- func (*PgStore) AdvancePlan(context.Context, string, int64) error +- func (*PgStore) DuePlans(context.Context, int64, int) ([]RacePlan, error) +- func (*PgStore) Enqueue(context.Context, string, string, string) (QueueEntry, error) +- func (*PgStore) Dequeue(context.Context, string, string) error +- func (*PgStore) ListQueueByDriver(context.Context, string) ([]QueueEntry, error) +- func (*PgStore) ListQueueByRace(context.Context, string) ([]QueueEntry, error) +- func (*PgStore) CountQueueByRace(context.Context, string) (int, error) +- func (FinishedRace) ToLobbyMeta() lobby.RaceMeta + +--- + +## Package `main` (`scripts/graphify`) + +Package main is the graphify utility. It walks the codebase, parses Go AST, +and outputs a highly compact symbol summary to server/CODEBASE.md. +This is used for token savings and helping developers/AI agents quickly map out imports and structures. + +### Structs +- **struct PackageSummary** { Name, Path, Doc, Structs, Interfaces, Functions, Methods } + +--- + +## Package `main` (`.`) + +Smoke test: ws client connects, exchanges a few messages, exits. +Run: go run ./smoke_test.go + +--- + +## Package `transport` (`internal/transport`) + +Package transport defines the wire format between clients and the server. + +PoC: JSON for fast iteration. Specified by proto/client_server.proto for +the production binary protobuf format. + +TODO PoC: replace JSON with protobuf-generated structs (see +../proto/client_server.proto). + +### Structs +- **struct Envelope** { Type, Seq, TSMs, Payload } +- **struct ClientHello** { Version, Token, Locale, ClientID } +- **struct ServerHello** { SessionID, RaceTick, ServerVersion, Config } +- **struct JoinRace** { RaceID, CarSlot, CarName } +- **struct LeaveRace** { RaceID } +- **struct InputState** { Steering, Throttle, Brake, Gear, Buttons } +- **struct ChatMessage** { Text } +- **struct Ping** { ClientTSMs } +- **struct RaceSnapshot** { Tick, TSMs, Elapsed, Cars } +- **struct CarInfo** { ID, DriverName, X, Y, Heading, Speed, Lap, Sector, LastLapMs, BestLapMs, InputApplied, DNF } +- **struct InputAck** { Seq, AppliedAtMs, ServerTickMs } +- **struct Correction** { Tick, DeltaSteering, DeltaThrottle, Reason } +- **struct RaceEvent** { RaceID, Event, CarID, Sector, LapMs, TSMs } +- **struct Pong** { ClientTSMs, ServerTSMs } +- **struct ErrorMsg** { Code, Message } +- **struct LobbyRace** { ID, Name, TrackID, MaxCars, Laps, TimeLimitS, DriverIDs, Status, CreatedMs, StartedMs, Podium } +- **struct RacePodiumEntry** { Position, DriverID, Name, TotalTimeMs, Nickname, AvatarUrl, ClanID } +- **struct VersionResponse** { Version } +- **struct WebRTCConnectRequest** { SDP, Type, DriverID } +- **struct WebRTCConnectResponse** { SDP, Type } +- **struct LobbyDriver** { ID, Name, Status, RaceID, ConnectedMs, LastSeenMs, Country, AvatarHue } +- **struct LobbySnapshot** { GeneratedMs, Version, Races, Drivers } +- **struct LobbyCreate** { RaceID, Name, TrackID, MaxCars, Laps, TimeLimitS } +- **struct LobbyJoinRace** { RaceID, CarSlot, CarName } +- **struct LobbyLeaveRace** { RaceID } +- **struct LobbyKickDriver** { RaceID, DriverID } +- **struct LobbyDelete** { RaceID } +- **struct LobbyCreatedAck** { Ok, Race, Error, Driver } +- **struct LobbyJoinAck** { Ok, Race, Error } +- **struct TrackWaypoint** { X, Y, HeadingRad, SpeedMs, Curvature } +- **struct TrackBounds** { MinX, MinY, MaxX, MaxY, LengthM, WidthM } +- **struct TrackWire** { ID, Name, Description, AuthorID, Visibility, Scale, LengthM, WidthM, LaneWidthM, Bounds, Surface, Tags, Centerline, BestLapMs, BestLapHolder, CreatedMs, UpdatedMs, IsActive } +- **struct TrackListRequest** { Scope, Tag } +- **struct TrackGetRequest** { ID } +- **struct TrackCreateRequest** { Track } +- **struct TrackUpdateRequest** { ID, Patch } +- **struct TrackPatch** { Name, Description, Visibility, Scale, LengthM, WidthM, LaneWidthM, Surface, Tags, Centerline } +- **struct TrackDeleteRequest** { ID } +- **struct TrackSnapshot** { GeneratedMs, Version, Tracks } +- **struct TrackAck** { Ok, ID, Error, Track } +- **struct MotorWire** { Kind, Class, KV, PowerW } +- **struct BatteryWire** { VoltageV, CapacityMah, Cells, Chemistry } +- **struct ChassisWire** { Model, Material, Printed, WheelbaseMm, TrackMm } +- **struct CarWire** { ID, Name, OwnerID, Visibility, Scale, LengthMm, WidthMm, HeightMm, WeightG, Chassis, Motor, Battery, Drive, TopSpeedMs, ColorHex, AvatarURL, Active, TotalDistanceM, TotalRaces, TotalLaps, BestLapMs, CreatedMs, UpdatedMs, DeviceID } +- **struct CarListRequest** { Scope, OwnerID } +- **struct CarGetRequest** { ID } +- **struct CarCreateRequest** { Car } +- **struct CarUpdateRequest** { ID, Patch } +- **struct CarPatch** { Name, Visibility, Scale, LengthMm, WidthMm, HeightMm, WeightG, Chassis, Motor, Battery, Drive, TopSpeedMs, ColorHex, AvatarURL, Active, DeviceID } +- **struct CarDeleteRequest** { ID } +- **struct CarSnapshot** { GeneratedMs, Version, Cars } +- **struct CarAck** { Ok, ID, Error, Car } +- **struct CatalogSummary** { TracksTotal, TracksSystem, TracksPublic, TracksPrivate, CarsTotal, CarsSystem, CarsPublic, CarsPrivate, CarsActive, TSMs } +- **struct RaceListResponse** { Items, NextCursor, Count } +- **struct RaceUpcomingItem** { Meta, QueueLen, PlanID, StartAtMs } +- **struct RaceUpcomingResponse** { Items, Count } +- **struct RaceQueueEntry** { DriverID, RaceID, PlanID, EnqueuedMs } +- **struct RaceQueueJoinRequest** { DriverID, Limit } +- **struct RaceQueueJoinResponse** { Joined, Skipped } +- **struct RaceQueueListResponse** { Items, Count } +- **struct RacePlan** { ID, Name, TrackID, MaxCars, Laps, TimeLimitS, StartAtMs, IntervalS, Count, Enabled, CreatedMs, UpdatedMs, NextFireMs, FiresDone } +- **struct RacePlanCreateRequest** { ID, Name, TrackID, MaxCars, Laps, TimeLimitS, StartAtMs, IntervalS, Count, Enabled } +- **struct RacePlanListResponse** { Items, NextCursor, Count } +- **struct Driver** { ID, Nickname, Name, AvatarURL, ClanID, CreatedMs, UpdatedMs } +- **struct DriverCreateRequest** { ID, Nickname, Name, AvatarURL, ClanID } +- **struct DriverUpdateRequest** { Name, AvatarURL, ClanID } +- **struct DriverListResponse** { Items, Count } +- **struct Clan** { ID, Tag, Name, AvatarURL, CreatedMs, UpdatedMs } +- **struct ClanCreateRequest** { ID, Tag, Name, AvatarURL } +- **struct ClanUpdateRequest** { Name, AvatarURL } +- **struct ClanListResponse** { Items, Count } +- **struct StatsServer** { StartedMs, UptimeMs, CurrentClients, PeakClients, TotalConnections, TotalDisconnects, TotalRacesCreated, TotalRacesStarted, TotalRacesFinished, TotalLapsRecorded, TotalInputsRecv, TotalSnapshotsSent, SnapshotsDropped, AvgRTTMs } +- **struct StatsDriver** { ID, Name, TotalRacesStarted, TotalRacesFinished, TotalLaps, BestLapMs, LastLapMs, TotalDistanceM, TotalPlaytimeMs, Wins, Podiums, LastSeenMs } +- **struct StatsLap** { RaceID, DriverID, LapMs, Sector1Ms, Sector2Ms, Sector3Ms, Position, TSMs } +- **struct StatsRaceResult** { RaceID, DriverID, FinishedAtMs, Position, TotalLaps, BestLapMs, TotalTimeMs, DNF } +- **struct StatsSnapshot** { Server, Drivers, Laps, Results, TSMs } +- **struct StatsRequest** { Scope, TargetID } +- **struct RaceStanding** { Position, CarID, DriverID, DriverName, Lap, Sector, BestLapMs, LastLapMs, GapMs, DNF } +- **struct RaceStandingsMsg** { RaceID, Tick, TSMs, Lap, Elapsed, Rows } +- **struct LapRecordedMsg** { RaceID, DriverID, Lap, LapMs, Sector1Ms, Sector2Ms, Sector3Ms, TSMs, Position } +- **struct RaceResultMsg** { RaceID, DriverID, Position, TotalLaps, BestLapMs, TotalTimeMs, DNF, FinishedAtMs } +- **struct LeaderboardEntry** { DriverID, Nickname, Name, AvatarURL, ClanID, ClanTag, Points, BestPos, BestTimeMs, Rank } +- **struct LeaderboardResponse** { Items, Total, Limit, Offset, CurrentDriver } +- **struct TrackCalendarEntry** { ID, TrackID, StartAt, EndAt } +- **struct TrackCalendarCreateRequest** { TrackID, StartAt, EndAt } +- **struct TrackCalendarResponse** { Items } +- **struct TrackCalendarLeaderboard** { TrackID, TrackName, StartAt, EndAt, Status, Podium, CurrentDriver } +- **struct TrackCalendarLeaderboardResponse** { Items } + +### Functions +- func Encode(*Envelope) ([]byte, error) +- func Decode([]byte) (*Envelope, error) + +--- + +## Package `main` (`cmd/poc-server`) + +HTTP handlers for the catalog (tracks + cars). + +All HTTP routes live under /api/*: + GET /api/tracks list tracks (optional ?scope=system|public|private&tag=) + GET /api/tracks/{id} get a single track + POST /api/tracks create a track + PUT /api/tracks/{id} update a track + DELETE /api/tracks/{id} delete a track + GET /api/cars list cars (optional ?scope=...&owner_id=...) + GET /api/cars/{id} get a single car + POST /api/cars create a car + PUT /api/cars/{id} update a car + DELETE /api/cars/{id} delete a car + GET /api/catalog combined snapshot + summary + +WebSocket access is wired up directly in main.go's readPump switch. + +### Structs +- **struct Hub** +- **struct UDPVideoService** +- **struct WebRTCService** + +### Functions +- func NewHub(*slog.Logger) *Hub +- func NewUDPVideoService(*slog.Logger, string, int) *UDPVideoService +- func NewWebRTCService(*slog.Logger, *control.Engine, *lobby.Service, *UDPVideoService, *catalog.Service) *WebRTCService + +### Methods +- func (*catalogBroadcaster) Run() +- func (*catalogBroadcaster) Stop() +- func (*Hub) BroadcastSnapshot(transport.RaceSnapshot) +- func (*statusRecorder) WriteHeader(int) +- func (*statusRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) +- func (*UDPVideoService) Start(context.Context, *sync.WaitGroup) +- func (*UDPVideoService) SendControlPacket(uint8, []byte) error +- func (*UDPVideoService) SendCommand(uint8, string) error +- func (*UDPVideoService) StreamHandler() http.HandlerFunc +- func (*UDPVideoService) ControlHandler() http.HandlerFunc +- func (*WebRTCService) Start(context.Context, *sync.WaitGroup) +- func (*WebRTCService) ConnectHandler() http.HandlerFunc +- func (*WebRTCService) CloseSession(string) +- func (*WebRTCService) CloseAll() + +--- + +## Package `main` (`scripts/genseed`) + +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` (`scripts/genseed-json`) + +Package main contains a small helper that prints the default seeds +(5 F1 tracks + 5 F1 cars) as JSON to stdout. It is run as: + + go run ./scripts/genseed-json > seeds.json + +which is then fed into scripts/genseed to regenerate the SQL. + +--- + diff --git a/server/Makefile b/server/Makefile index 0d281db..0591c27 100644 --- a/server/Makefile +++ b/server/Makefile @@ -84,4 +84,9 @@ compose-down: clean: rm -rf $(BIN_DIR) +.PHONY: graphify +graphify: + $(GO) run ./scripts/graphify/main.go + .DEFAULT_GOAL := build + diff --git a/server/internal/clans/service.go b/server/internal/clans/service.go index 3a16e2d..910a626 100644 --- a/server/internal/clans/service.go +++ b/server/internal/clans/service.go @@ -10,14 +10,14 @@ import ( "github.com/jackc/pgx/v5/pgconn" ) -// Service composes validation on top of PgStore. +// Service composes validation on top of Store. type Service struct { - pg *PgStore + pg Store now func() time.Time } // NewService wires a service. -func NewService(pg *PgStore) *Service { +func NewService(pg Store) *Service { return &Service{pg: pg, now: time.Now} } diff --git a/server/internal/clans/service_test.go b/server/internal/clans/service_test.go new file mode 100644 index 0000000..033f43b --- /dev/null +++ b/server/internal/clans/service_test.go @@ -0,0 +1,166 @@ +package clans + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/jackc/pgx/v5/pgconn" +) + +type memoryStore struct { + clans map[string]Clan +} + +func newMemoryStore() *memoryStore { + return &memoryStore{clans: make(map[string]Clan)} +} + +func (m *memoryStore) Exec(ctx context.Context, sql string) (int64, error) { + return 0, nil +} + +func (m *memoryStore) Create(ctx context.Context, c Clan) error { + for _, existing := range m.clans { + if existing.Tag == c.Tag { + return &pgconn.PgError{Code: "23505", Message: "duplicate key value violates unique constraint"} + } + } + m.clans[c.ID] = c + return nil +} + +func (m *memoryStore) Get(ctx context.Context, id string) (Clan, error) { + c, ok := m.clans[id] + if !ok { + return Clan{}, ErrNotFound + } + return c, nil +} + +func (m *memoryStore) GetByTag(ctx context.Context, tag string) (Clan, error) { + for _, c := range m.clans { + if c.Tag == tag { + return c, nil + } + } + return Clan{}, ErrNotFound +} + +func (m *memoryStore) List(ctx context.Context, limit, offset int) ([]Clan, error) { + out := make([]Clan, 0) + for _, c := range m.clans { + out = append(out, c) + } + return out, nil +} + +func (m *memoryStore) Delete(ctx context.Context, id string) error { + if _, ok := m.clans[id]; !ok { + return ErrNotFound + } + delete(m.clans, id) + return nil +} + +func (m *memoryStore) Update(ctx context.Context, c Clan) error { + if _, ok := m.clans[c.ID]; !ok { + return ErrNotFound + } + m.clans[c.ID] = c + return nil +} + +func (m *memoryStore) Count(ctx context.Context) (int, error) { + return len(m.clans), nil +} + +func TestClansService(t *testing.T) { + store := newMemoryStore() + svc := NewService(store) + svc.now = func() time.Time { + return time.Unix(1700000000, 0) + } + ctx := context.Background() + + // 1. Create a clan with valid inputs + c1, err := svc.Create(ctx, CreateInput{ + ID: "clan-1", + Tag: "SPD", + Name: "Speed Demons", + AvatarURL: "https://example.com/spd.png", + }) + if err != nil { + t.Fatalf("Create failed: %v", err) + } + if c1.ID != "clan-1" || c1.Tag != "SPD" || c1.Name != "Speed Demons" { + t.Errorf("Unexpected created clan: %+v", c1) + } + + // 2. Validate Tag must be 3 uppercase ASCII letters + _, err = svc.Create(ctx, CreateInput{Tag: "SPEED", Name: "toolongtag"}) + if !errors.Is(err, ErrInvalidInput) { + t.Errorf("Expected ErrInvalidInput for tag 'SPEED', got: %v", err) + } + _, err = svc.Create(ctx, CreateInput{Tag: "S1D", Name: "withnumbertag"}) + if !errors.Is(err, ErrInvalidInput) { + t.Errorf("Expected ErrInvalidInput for tag 'S1D', got: %v", err) + } + + // 3. Validate empty Name is rejected + _, err = svc.Create(ctx, CreateInput{Tag: "AAA", Name: ""}) + if !errors.Is(err, ErrInvalidInput) { + t.Errorf("Expected ErrInvalidInput for empty name, got: %v", err) + } + + // 4. Duplicate tag check + _, err = svc.Create(ctx, CreateInput{ + ID: "clan-2", + Tag: "SPD", + Name: "Duplicate SPD Tag", + }) + if !errors.Is(err, ErrAlreadyExists) { + t.Errorf("Expected ErrAlreadyExists for duplicate tag, got: %v", err) + } + + // 5. Get clan + got, err := svc.Get(ctx, "clan-1") + if err != nil { + t.Fatalf("Get failed: %v", err) + } + if got.Tag != "SPD" { + t.Errorf("Expected SPD, got %s", got.Tag) + } + + // 6. Get by tag + gotByTag, err := svc.GetByTag(ctx, "spd") // should normalize to SPD + if err != nil { + t.Fatalf("GetByTag failed: %v", err) + } + if gotByTag.ID != "clan-1" { + t.Errorf("Expected clan-1, got %s", gotByTag.ID) + } + + // 7. Update clan + updated, err := svc.Update(ctx, "clan-1", UpdateInput{ + Name: "Super Speed Demons", + AvatarURL: "https://example.com/super-spd.png", + }) + if err != nil { + t.Fatalf("Update failed: %v", err) + } + if updated.Name != "Super Speed Demons" || updated.AvatarURL != "https://example.com/super-spd.png" { + t.Errorf("Unexpected updated clan fields: %+v", updated) + } + + // 8. Delete clan + err = svc.Delete(ctx, "clan-1") + if err != nil { + t.Fatalf("Delete failed: %v", err) + } + _, err = svc.Get(ctx, "clan-1") + if !errors.Is(err, ErrNotFound) { + t.Errorf("Expected ErrNotFound after deletion, got: %v", err) + } +} diff --git a/server/internal/clans/store.go b/server/internal/clans/store.go index efca8c2..46fd614 100644 --- a/server/internal/clans/store.go +++ b/server/internal/clans/store.go @@ -39,6 +39,18 @@ type Clan struct { UpdatedMs int64 } +// Store defines the persistence operations for clans. +type Store interface { + Exec(ctx context.Context, sql string) (int64, error) + Create(ctx context.Context, c Clan) error + Get(ctx context.Context, id string) (Clan, error) + GetByTag(ctx context.Context, tag string) (Clan, error) + List(ctx context.Context, limit, offset int) ([]Clan, error) + Delete(ctx context.Context, id string) error + Update(ctx context.Context, c Clan) error + Count(ctx context.Context) (int, error) +} + // PgStore is the Postgres-backed half of the package. type PgStore struct { pool *pgxpool.Pool diff --git a/server/internal/drivers/service.go b/server/internal/drivers/service.go index e7a9449..bfb4097 100644 --- a/server/internal/drivers/service.go +++ b/server/internal/drivers/service.go @@ -10,14 +10,14 @@ import ( "github.com/jackc/pgx/v5/pgconn" ) -// Service composes validation on top of PgStore. +// Service composes validation on top of Store. type Service struct { - pg *PgStore + pg Store now func() time.Time } // NewService wires a service. -func NewService(pg *PgStore) *Service { +func NewService(pg Store) *Service { return &Service{pg: pg, now: time.Now} } diff --git a/server/internal/drivers/service_test.go b/server/internal/drivers/service_test.go new file mode 100644 index 0000000..f07c9c7 --- /dev/null +++ b/server/internal/drivers/service_test.go @@ -0,0 +1,167 @@ +package drivers + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/jackc/pgx/v5/pgconn" +) + +type memoryStore struct { + drivers map[string]Driver +} + +func newMemoryStore() *memoryStore { + return &memoryStore{drivers: make(map[string]Driver)} +} + +func (m *memoryStore) Exec(ctx context.Context, sql string) (int64, error) { + return 0, nil +} + +func (m *memoryStore) Create(ctx context.Context, d Driver) error { + for _, existing := range m.drivers { + if existing.Nickname == d.Nickname { + return &pgconn.PgError{Code: "23505", Message: "duplicate key value violates unique constraint"} + } + } + m.drivers[d.ID] = d + return nil +} + +func (m *memoryStore) Get(ctx context.Context, id string) (Driver, error) { + d, ok := m.drivers[id] + if !ok { + return Driver{}, ErrNotFound + } + return d, nil +} + +func (m *memoryStore) GetByNickname(ctx context.Context, nick string) (Driver, error) { + for _, d := range m.drivers { + if d.Nickname == nick { + return d, nil + } + } + return Driver{}, ErrNotFound +} + +func (m *memoryStore) List(ctx context.Context, limit, offset int, clanID string) ([]Driver, error) { + out := make([]Driver, 0) + for _, d := range m.drivers { + if clanID == "" || d.ClanID == clanID { + out = append(out, d) + } + } + return out, nil +} + +func (m *memoryStore) Delete(ctx context.Context, id string) error { + if _, ok := m.drivers[id]; !ok { + return ErrNotFound + } + delete(m.drivers, id) + return nil +} + +func (m *memoryStore) Update(ctx context.Context, d Driver) error { + if _, ok := m.drivers[d.ID]; !ok { + return ErrNotFound + } + m.drivers[d.ID] = d + return nil +} + +func (m *memoryStore) Count(ctx context.Context) (int, error) { + return len(m.drivers), nil +} + +func TestDriversService(t *testing.T) { + store := newMemoryStore() + svc := NewService(store) + svc.now = func() time.Time { + return time.Unix(1700000000, 0) + } + ctx := context.Background() + + // 1. Create a driver + d1, err := svc.Create(ctx, CreateInput{ + ID: "driver-1", + Nickname: "HAM", + Name: "Lewis Hamilton", + AvatarURL: "https://example.com/ham.png", + ClanID: "clan-1", + }) + if err != nil { + t.Fatalf("Create failed: %v", err) + } + if d1.ID != "driver-1" || d1.Nickname != "HAM" || d1.Name != "Lewis Hamilton" || d1.ClanID != "clan-1" { + t.Errorf("Unexpected created driver: %+v", d1) + } + + // 2. Validate Nickname must be 3 uppercase ASCII letters + _, err = svc.Create(ctx, CreateInput{Nickname: "HAMILTON", Name: "toolong"}) + if !errors.Is(err, ErrInvalidInput) { + t.Errorf("Expected ErrInvalidInput for tag 'HAMILTON', got: %v", err) + } + + // 3. Validate empty Name is rejected + _, err = svc.Create(ctx, CreateInput{Nickname: "BOT", Name: ""}) + if !errors.Is(err, ErrInvalidInput) { + t.Errorf("Expected ErrInvalidInput for empty name, got: %v", err) + } + + // 4. Duplicate nickname check + _, err = svc.Create(ctx, CreateInput{ + ID: "driver-2", + Nickname: "HAM", + Name: "Another Lewis", + }) + if !errors.Is(err, ErrAlreadyExists) { + t.Errorf("Expected ErrAlreadyExists for duplicate nickname, got: %v", err) + } + + // 5. Get driver + got, err := svc.Get(ctx, "driver-1") + if err != nil { + t.Fatalf("Get failed: %v", err) + } + if got.Nickname != "HAM" { + t.Errorf("Expected HAM, got %s", got.Nickname) + } + + // 6. Get by nickname + gotByNick, err := svc.GetByNickname(ctx, "ham") // should normalize to HAM + if err != nil { + t.Fatalf("GetByNickname failed: %v", err) + } + if gotByNick.ID != "driver-1" { + t.Errorf("Expected driver-1, got %s", gotByNick.ID) + } + + // 7. Update driver + clanID := "clan-2" + updated, err := svc.Update(ctx, "driver-1", UpdateInput{ + Name: "Lewis Hamilton MBE", + AvatarURL: "https://example.com/ham-mbe.png", + ClanID: &clanID, + }) + if err != nil { + t.Fatalf("Update failed: %v", err) + } + if updated.Name != "Lewis Hamilton MBE" || updated.AvatarURL != "https://example.com/ham-mbe.png" || updated.ClanID != "clan-2" { + t.Errorf("Unexpected updated driver fields: %+v", updated) + } + + // 8. Delete driver + err = svc.Delete(ctx, "driver-1") + if err != nil { + t.Fatalf("Delete failed: %v", err) + } + _, err = svc.Get(ctx, "driver-1") + if !errors.Is(err, ErrNotFound) { + t.Errorf("Expected ErrNotFound after deletion, got: %v", err) + } +} diff --git a/server/internal/drivers/store.go b/server/internal/drivers/store.go index 4fa6a4f..f5efd13 100644 --- a/server/internal/drivers/store.go +++ b/server/internal/drivers/store.go @@ -39,6 +39,18 @@ type Driver struct { UpdatedMs int64 } +// Store defines the persistence operations for drivers. +type Store interface { + Exec(ctx context.Context, sql string) (int64, error) + Create(ctx context.Context, d Driver) error + Get(ctx context.Context, id string) (Driver, error) + GetByNickname(ctx context.Context, nick string) (Driver, error) + List(ctx context.Context, limit, offset int, clanID string) ([]Driver, error) + Delete(ctx context.Context, id string) error + Update(ctx context.Context, d Driver) error + Count(ctx context.Context) (int, error) +} + // PgStore is the Postgres-backed half of the package. type PgStore struct { pool *pgxpool.Pool diff --git a/server/scripts/graphify/main.go b/server/scripts/graphify/main.go new file mode 100644 index 0000000..9f89ea5 --- /dev/null +++ b/server/scripts/graphify/main.go @@ -0,0 +1,250 @@ +// Package main is the graphify utility. It walks the codebase, parses Go AST, +// and outputs a highly compact symbol summary to server/CODEBASE.md. +// This is used for token savings and helping developers/AI agents quickly map out imports and structures. +package main + +import ( + "bytes" + "fmt" + "go/ast" + "go/parser" + "go/token" + "io/fs" + "os" + "path/filepath" + "strings" +) + +type PackageSummary struct { + Name string + Path string + Doc string + Structs []string + Interfaces []string + Functions []string + Methods []string +} + +func main() { + rootDir := "." + outputFile := "CODEBASE.md" + + var buf bytes.Buffer + buf.WriteString("# x0gp Go Codebase Map (Generated)\n\n") + buf.WriteString("This file contains a compiled map of all packages, structures, interfaces, and public functions in the `server` directory. It is optimized for token-efficiency so AI agents and humans can understand the codebase layout without reading all files.\n\n") + + pkgs := make(map[string]*PackageSummary) + fset := token.NewFileSet() + + err := filepath.WalkDir(rootDir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + // Skip hidden and build dirs + name := d.Name() + if (name != "." && strings.HasPrefix(name, ".")) || name == "vendor" || name == "bin" || name == "docs" || name == "web" { + return filepath.SkipDir + } + return nil + } + + if !strings.HasSuffix(d.Name(), ".go") || strings.HasSuffix(d.Name(), "_test.go") { + return nil + } + + dir := filepath.Dir(path) + pkgSum, exists := pkgs[dir] + if !exists { + pkgSum = &PackageSummary{ + Path: dir, + } + pkgs[dir] = pkgSum + } + + node, err := parser.ParseFile(fset, path, nil, parser.ParseComments) + if err != nil { + return nil // ignore unparseable files defensively + } + + pkgSum.Name = node.Name.Name + if node.Doc != nil && pkgSum.Doc == "" { + pkgSum.Doc = strings.TrimSpace(node.Doc.Text()) + } + + // Inspect AST for declarations + ast.Inspect(node, func(n ast.Node) bool { + switch decl := n.(type) { + case *ast.GenDecl: + for _, spec := range decl.Specs { + if typeSpec, ok := spec.(*ast.TypeSpec); ok { + if !ast.IsExported(typeSpec.Name.Name) { + continue + } + switch typeType := typeSpec.Type.(type) { + case *ast.StructType: + structDesc := fmt.Sprintf("- **struct %s**", typeSpec.Name.Name) + if typeType.Fields != nil && len(typeType.Fields.List) > 0 { + fields := []string{} + for _, f := range typeType.Fields.List { + for _, id := range f.Names { + if ast.IsExported(id.Name) { + fields = append(fields, id.Name) + } + } + } + if len(fields) > 0 { + structDesc += fmt.Sprintf(" { %s }", strings.Join(fields, ", ")) + } + } + pkgSum.Structs = append(pkgSum.Structs, structDesc) + case *ast.InterfaceType: + methods := []string{} + if typeType.Methods != nil { + for _, m := range typeType.Methods.List { + for _, id := range m.Names { + if ast.IsExported(id.Name) { + methods = append(methods, id.Name) + } + } + } + } + pkgSum.Interfaces = append(pkgSum.Interfaces, fmt.Sprintf("- **interface %s** { %s }", typeSpec.Name.Name, strings.Join(methods, ", "))) + } + } + } + case *ast.FuncDecl: + if !ast.IsExported(decl.Name.Name) { + return true + } + // Format function signature + params := []string{} + if decl.Type.Params != nil { + for _, p := range decl.Type.Params.List { + for range p.Names { + params = append(params, formatType(p.Type)) + } + if len(p.Names) == 0 { + params = append(params, formatType(p.Type)) + } + } + } + results := []string{} + if decl.Type.Results != nil { + for _, r := range decl.Type.Results.List { + for range r.Names { + results = append(results, formatType(r.Type)) + } + if len(r.Names) == 0 { + results = append(results, formatType(r.Type)) + } + } + } + + sig := fmt.Sprintf("%s(%s)", decl.Name.Name, strings.Join(params, ", ")) + if len(results) > 0 { + if len(results) == 1 { + sig += " " + results[0] + } else { + sig += " (" + strings.Join(results, ", ") + ")" + } + } + + if decl.Recv != nil && len(decl.Recv.List) > 0 { + recvType := formatType(decl.Recv.List[0].Type) + pkgSum.Methods = append(pkgSum.Methods, fmt.Sprintf("- func (%s) %s", recvType, sig)) + } else { + pkgSum.Functions = append(pkgSum.Functions, fmt.Sprintf("- func %s", sig)) + } + } + return true + }) + + return nil + }) + + if err != nil { + fmt.Fprintf(os.Stderr, "Error gathering package summaries: %v\n", err) + os.Exit(1) + } + + // Write out gathered summaries sorted/grouped by path + // (we can sort keys or print them grouped by internal vs cmd) + paths := []string{} + for p := range pkgs { + paths = append(paths, p) + } + // Simple sort: cmd/ first, then internal/ + // We want to present it nicely + for _, path := range paths { + sum := pkgs[path] + cleanPath := filepath.ToSlash(path) + buf.WriteString(fmt.Sprintf("## Package `%s` (`%s`)\n\n", sum.Name, cleanPath)) + if sum.Doc != "" { + buf.WriteString(fmt.Sprintf("%s\n\n", sum.Doc)) + } + + if len(sum.Interfaces) > 0 { + buf.WriteString("### Interfaces\n") + for _, item := range sum.Interfaces { + buf.WriteString(item + "\n") + } + buf.WriteString("\n") + } + + if len(sum.Structs) > 0 { + buf.WriteString("### Structs\n") + for _, item := range sum.Structs { + buf.WriteString(item + "\n") + } + buf.WriteString("\n") + } + + if len(sum.Functions) > 0 { + buf.WriteString("### Functions\n") + for _, item := range sum.Functions { + buf.WriteString(item + "\n") + } + buf.WriteString("\n") + } + + if len(sum.Methods) > 0 { + buf.WriteString("### Methods\n") + for _, item := range sum.Methods { + buf.WriteString(item + "\n") + } + buf.WriteString("\n") + } + + buf.WriteString("---\n\n") + } + + err = os.WriteFile(outputFile, buf.Bytes(), 0644) + if err != nil { + fmt.Fprintf(os.Stderr, "Error writing CODEBASE.md: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Generated %s successfully!\n", outputFile) +} + +func formatType(expr ast.Expr) string { + switch t := expr.(type) { + case *ast.Ident: + return t.Name + case *ast.StarExpr: + return "*" + formatType(t.X) + case *ast.SelectorExpr: + return formatType(t.X) + "." + t.Sel.Name + case *ast.ArrayType: + return "[]" + formatType(t.Elt) + case *ast.MapType: + return "map[" + formatType(t.Key) + "]" + formatType(t.Value) + case *ast.InterfaceType: + return "interface{}" + case *ast.FuncType: + return "func" + default: + return "?" + } +} diff --git a/supabase/.gitignore b/supabase/.gitignore new file mode 100644 index 0000000..ad9264f --- /dev/null +++ b/supabase/.gitignore @@ -0,0 +1,8 @@ +# Supabase +.branches +.temp + +# dotenvx +.env.keys +.env.local +.env.*.local diff --git a/supabase/config.toml b/supabase/config.toml new file mode 100644 index 0000000..d029db5 --- /dev/null +++ b/supabase/config.toml @@ -0,0 +1,414 @@ +# For detailed configuration reference documentation, visit: +# https://supabase.com/docs/guides/local-development/cli/config +# A string used to distinguish different Supabase projects on the same host. Defaults to the +# working directory name when running `supabase init`. +project_id = "x0gp" + +[api] +enabled = true +# Port to use for the API URL. +port = 54321 +# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API +# endpoints. `public` and `graphql_public` schemas are included by default. +schemas = ["public", "graphql_public"] +# Extra schemas to add to the search_path of every request. +extra_search_path = ["public", "extensions"] +# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size +# for accidental or malicious requests. +max_rows = 1000 +# Controls whether new tables, views, sequences and functions created in the `public` schema by +# `postgres` are reachable through the Data API roles (`anon`, `authenticated`, `service_role`) +# without explicit GRANTs. When unset, new entities are NOT auto-exposed, matching the new cloud +# default. Set to `true` to keep the legacy behaviour of auto-exposing new entities; this is +# deprecated and the field is removed on 2026-10-30 once the always-revoked behaviour is permanent. +# auto_expose_new_tables = true + +[api.tls] +# Enable HTTPS endpoints locally using a self-signed certificate. +enabled = false +# Paths to self-signed certificate pair. +# cert_path = "../certs/my-cert.pem" +# key_path = "../certs/my-key.pem" + +[db] +# Port to use for the local database URL. +port = 54322 +# Port used by db diff command to initialize the shadow database. +shadow_port = 54320 +# Maximum amount of time to wait for health check when starting the local database. +health_timeout = "2m" +# The database major version to use. This has to be the same as your remote database's. Run `SHOW +# server_version;` on the remote database to check. +major_version = 17 + +[db.pooler] +enabled = false +# Port to use for the local connection pooler. +port = 54329 +# Specifies when a server connection can be reused by other clients. +# Configure one of the supported pooler modes: `transaction`, `session`. +pool_mode = "transaction" +# How many server connections to allow per user/database pair. +default_pool_size = 20 +# Maximum number of client connections allowed. +max_client_conn = 100 + +# [db.vault] +# secret_key = "env(SECRET_VALUE)" + +[db.migrations] +# If disabled, migrations will be skipped during a db push or reset. +enabled = true +# Specifies an ordered list of schema files, directories, or glob patterns that describe your database. +# Supports paths relative to supabase directory: "./schemas/*.sql", "./database". +schema_paths = [] + +[db.seed] +# If enabled, seeds the database after migrations during a db reset. +enabled = true +# Specifies an ordered list of seed files to load during db reset. +# Supports glob patterns relative to supabase directory: "./seeds/*.sql" +sql_paths = ["./seed.sql"] + +[db.network_restrictions] +# Enable management of network restrictions. +enabled = false +# List of IPv4 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv4 connections. Set empty array to block all IPs. +allowed_cidrs = ["0.0.0.0/0"] +# List of IPv6 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv6 connections. Set empty array to block all IPs. +allowed_cidrs_v6 = ["::/0"] + +# Uncomment to reject non-secure connections to the database. +# [db.ssl_enforcement] +# enabled = true + +[realtime] +enabled = true +# Bind realtime via either IPv4 or IPv6. (default: IPv4) +# ip_version = "IPv6" +# The maximum length in bytes of HTTP request headers. (default: 4096) +# max_header_length = 4096 + +[studio] +enabled = true +# Port to use for Supabase Studio. +port = 54323 +# External URL of the API server that frontend connects to. +api_url = "http://127.0.0.1" +# OpenAI API Key to use for Supabase AI in the Supabase Studio. +openai_api_key = "env(OPENAI_API_KEY)" + +# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they +# are monitored, and you can view the emails that would have been sent from the web interface. +[local_smtp] +enabled = true +# Port to use for the email testing server web interface. +port = 54324 +# Uncomment to expose additional ports for testing user applications that send emails. +# smtp_port = 54325 +# pop3_port = 54326 +# admin_email = "admin@email.com" +# sender_name = "Admin" + +[storage] +enabled = true +# The maximum file size allowed (e.g. "5MB", "500KB"). +file_size_limit = "50MiB" + +# Uncomment to configure local storage buckets +# [storage.buckets.images] +# public = false +# file_size_limit = "50MiB" +# allowed_mime_types = ["image/png", "image/jpeg"] +# objects_path = "./images" + +# Allow connections via S3 compatible clients +[storage.s3_protocol] +enabled = true + +# Image transformation API is available to Supabase Pro plan. +# [storage.image_transformation] +# enabled = true + +# Store analytical data in S3 for running ETL jobs over Iceberg Catalog +# This feature is only available on the hosted platform. +[storage.analytics] +enabled = false +max_namespaces = 5 +max_tables = 10 +max_catalogs = 2 + +# Analytics Buckets is available to Supabase Pro plan. +# [storage.analytics.buckets.my-warehouse] + +# Store vector embeddings in S3 for large and durable datasets +[storage.vector] +enabled = true +max_buckets = 10 +max_indexes = 5 + +# Vector Buckets is available to Supabase Pro plan. +# [storage.vector.buckets.documents-openai] + +[auth] +enabled = true +# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used +# in emails. +site_url = "http://127.0.0.1:3000" +# The public URL that Auth serves on. Defaults to the API external URL with `/auth/v1` appended. +# external_url = "" +# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. +additional_redirect_urls = ["https://127.0.0.1:3000"] +# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). +jwt_expiry = 3600 +# JWT issuer URL. If not set, defaults to auth.external_url. +# jwt_issuer = "" +# Path to JWT signing key. DO NOT commit your signing keys file to git. +# signing_keys_path = "./signing_keys.json" +# If disabled, the refresh token will never expire. +enable_refresh_token_rotation = true +# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. +# Requires enable_refresh_token_rotation = true. +refresh_token_reuse_interval = 10 +# Allow/disallow new user signups to your project. +enable_signup = true +# Allow/disallow anonymous sign-ins to your project. +enable_anonymous_sign_ins = false +# Allow/disallow testing manual linking of accounts +enable_manual_linking = false +# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more. +minimum_password_length = 6 +# Passwords that do not meet the following requirements will be rejected as weak. Supported values +# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols` +password_requirements = "" + +# Configure passkey sign-ins. +# [auth.passkey] +# enabled = false + +# Configure WebAuthn relying party settings (required when passkey is enabled). +# [auth.webauthn] +# rp_display_name = "Supabase" +# rp_id = "localhost" +# rp_origins = ["http://127.0.0.1:3000"] + +[auth.rate_limit] +# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled. +email_sent = 2 +# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled. +sms_sent = 30 +# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true. +anonymous_users = 30 +# Number of sessions that can be refreshed in a 5 minute interval per IP address. +token_refresh = 150 +# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users). +sign_in_sign_ups = 30 +# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address. +token_verifications = 30 +# Number of Web3 logins that can be made in a 5 minute interval per IP address. +web3 = 30 + +# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`. +# [auth.captcha] +# enabled = true +# provider = "hcaptcha" +# secret = "" + +[auth.email] +# Allow/disallow new user signups via email to your project. +enable_signup = true +# If enabled, a user will be required to confirm any email change on both the old, and new email +# addresses. If disabled, only the new email is required to confirm. +double_confirm_changes = true +# If enabled, users need to confirm their email address before signing in. +enable_confirmations = false +# If enabled, users will need to reauthenticate or have logged in recently to change their password. +secure_password_change = false +# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. +max_frequency = "1s" +# Number of characters used in the email OTP. +otp_length = 6 +# Number of seconds before the email OTP expires (defaults to 1 hour). +otp_expiry = 3600 + +# Use a production-ready SMTP server +# [auth.email.smtp] +# enabled = true +# host = "smtp.sendgrid.net" +# port = 587 +# user = "apikey" +# pass = "env(SENDGRID_API_KEY)" +# admin_email = "admin@email.com" +# sender_name = "Admin" + +# Uncomment to customize email template +# [auth.email.template.invite] +# subject = "You have been invited" +# content_path = "./supabase/templates/invite.html" + +# Uncomment to customize notification email template +# [auth.email.notification.password_changed] +# enabled = true +# subject = "Your password has been changed" +# content_path = "./templates/password_changed_notification.html" + +[auth.sms] +# Allow/disallow new user signups via SMS to your project. +enable_signup = false +# If enabled, users need to confirm their phone number before signing in. +enable_confirmations = false +# Template for sending OTP to users +template = "Your code is {{ `{{ .Code }}` }}" +# Controls the minimum amount of time that must pass before sending another sms otp. +max_frequency = "5s" + +# Use pre-defined map of phone number to OTP for testing. +# [auth.sms.test_otp] +# 4152127777 = "123456" + +# Configure logged in session timeouts. +# [auth.sessions] +# Force log out after the specified duration. +# timebox = "24h" +# Force log out if the user has been inactive longer than the specified duration. +# inactivity_timeout = "8h" + +# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object. +# [auth.hook.before_user_created] +# enabled = true +# uri = "pg-functions://postgres/auth/before-user-created-hook" + +# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. +# [auth.hook.custom_access_token] +# enabled = true +# uri = "pg-functions:////" + +# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. +[auth.sms.twilio] +enabled = false +account_sid = "" +message_service_sid = "" +# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: +auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" + +# Multi-factor-authentication is available to Supabase Pro plan. +[auth.mfa] +# Control how many MFA factors can be enrolled at once per user. +max_enrolled_factors = 10 + +# Control MFA via App Authenticator (TOTP) +[auth.mfa.totp] +enroll_enabled = false +verify_enabled = false + +# Configure MFA via Phone Messaging +[auth.mfa.phone] +enroll_enabled = false +verify_enabled = false +otp_length = 6 +template = "Your code is {{ `{{ .Code }}` }}" +max_frequency = "5s" + +# Configure MFA via WebAuthn +# [auth.mfa.web_authn] +# enroll_enabled = true +# verify_enabled = true + +# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, +# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, +# `twitter`, `x`, `slack`, `spotify`, `workos`, `zoom`. +[auth.external.apple] +enabled = false +client_id = "" +# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: +secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" +# Overrides the default auth callback URL derived from auth.external_url. +redirect_uri = "" +# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, +# or any other third-party OIDC providers. +url = "" +# If enabled, the nonce check will be skipped. Required for local sign in with Google auth. +skip_nonce_check = false +# If enabled, it will allow the user to successfully authenticate when the provider does not return an email address. +email_optional = false + +# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard. +# You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting. +[auth.web3.solana] +enabled = false + +# Use Firebase Auth as a third-party provider alongside Supabase Auth. +[auth.third_party.firebase] +enabled = false +# project_id = "my-firebase-project" + +# Use Auth0 as a third-party provider alongside Supabase Auth. +[auth.third_party.auth0] +enabled = false +# tenant = "my-auth0-tenant" +# tenant_region = "us" + +# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth. +[auth.third_party.aws_cognito] +enabled = false +# user_pool_id = "my-user-pool-id" +# user_pool_region = "us-east-1" + +# Use Clerk as a third-party provider alongside Supabase Auth. +[auth.third_party.clerk] +enabled = false +# Obtain from https://clerk.com/setup/supabase +# domain = "example.clerk.accounts.dev" + +# OAuth server configuration +[auth.oauth_server] +# Enable OAuth server functionality +enabled = false +# Path for OAuth consent flow UI +authorization_url_path = "/oauth/consent" +# Allow dynamic client registration +allow_dynamic_registration = false + +[edge_runtime] +enabled = true +# Supported request policies: `oneshot`, `per_worker`. +# `per_worker` (default) — enables hot reload during local development. +# `oneshot` — fallback mode if hot reload causes issues (e.g. in large repos or with symlinks). +policy = "per_worker" +# Port to attach the Chrome inspector for debugging edge functions. +inspector_port = 8083 +# The Deno major version to use. +deno_version = 2 + +# [edge_runtime.secrets] +# secret_key = "env(SECRET_VALUE)" + +[analytics] +enabled = true +port = 54327 +# Configure one of the supported backends: `postgres`, `bigquery`. +backend = "postgres" + +# Experimental features may be deprecated any time +[experimental] +# Configures Postgres storage engine to use OrioleDB (S3) +orioledb_version = "" +# Configures S3 bucket URL, eg. .s3-.amazonaws.com +s3_host = "env(S3_HOST)" +# Configures S3 bucket region, eg. us-east-1 +s3_region = "env(S3_REGION)" +# Configures AWS_ACCESS_KEY_ID for S3 bucket +s3_access_key = "env(S3_ACCESS_KEY)" +# Configures AWS_SECRET_ACCESS_KEY for S3 bucket +s3_secret_key = "env(S3_SECRET_KEY)" + +# pg-delta is the schema diff engine for db diff / db pull / db remote commit. +# Set enabled = false to fall back to the legacy migra engine. +[experimental.pgdelta] +enabled = true +# Directory under `supabase/` where declarative files are written. +# declarative_schema_path = "./database" +# JSON string passed through to pg-delta SQL formatting. +# format_options = "{\"keywordCase\":\"upper\",\"indent\":2,\"maxWidth\":80,\"commaStyle\":\"trailing\"}" diff --git a/supabase/migrations/20260717000000_schema.sql b/supabase/migrations/20260717000000_schema.sql new file mode 100644 index 0000000..26b12a4 --- /dev/null +++ b/supabase/migrations/20260717000000_schema.sql @@ -0,0 +1,265 @@ +-- 20260717000000_schema.sql — Consolidated schema for x0gp catalog, races, and drivers. + +-- --------------------------------------------------------------------------- +-- Tracks & Waypoints +-- --------------------------------------------------------------------------- + +CREATE TABLE IF NOT EXISTS tracks ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + author_id TEXT NOT NULL DEFAULT 'system', + visibility TEXT NOT NULL DEFAULT 'system' CHECK (visibility IN ('system', 'public', 'private')), + scale INT NOT NULL DEFAULT 27, + length_m DOUBLE PRECISION NOT NULL DEFAULT 0, + width_m DOUBLE PRECISION NOT NULL DEFAULT 0, + lane_width_m DOUBLE PRECISION NOT NULL DEFAULT 0.20, + surface TEXT NOT NULL DEFAULT 'carpet' CHECK (surface IN ('carpet', 'tile', 'wood', 'paper', 'mixed')), + best_lap_ms BIGINT NOT NULL DEFAULT 0, + best_lap_holder TEXT NOT NULL DEFAULT '', + bounds_min_x DOUBLE PRECISION NOT NULL DEFAULT 0, + bounds_min_y DOUBLE PRECISION NOT NULL DEFAULT 0, + bounds_max_x DOUBLE PRECISION NOT NULL DEFAULT 0, + bounds_max_y DOUBLE PRECISION NOT NULL DEFAULT 0, + created_ms BIGINT NOT NULL, + updated_ms BIGINT NOT NULL +); + +CREATE INDEX IF NOT EXISTS tracks_visibility_idx ON tracks (visibility); +CREATE INDEX IF NOT EXISTS tracks_author_idx ON tracks (author_id); + +CREATE TABLE IF NOT EXISTS track_waypoints ( + track_id TEXT NOT NULL REFERENCES tracks(id) ON DELETE CASCADE, + seq INT NOT NULL, + x DOUBLE PRECISION NOT NULL, + y DOUBLE PRECISION NOT NULL, + heading_rad DOUBLE PRECISION NOT NULL, + speed_ms DOUBLE PRECISION NOT NULL, + curvature DOUBLE PRECISION NOT NULL, + PRIMARY KEY (track_id, seq) +); + +CREATE TABLE IF NOT EXISTS track_tags ( + track_id TEXT NOT NULL REFERENCES tracks(id) ON DELETE CASCADE, + tag TEXT NOT NULL, + PRIMARY KEY (track_id, tag) +); + +CREATE INDEX IF NOT EXISTS track_tags_tag_idx ON track_tags (tag); + +-- --------------------------------------------------------------------------- +-- Cars +-- --------------------------------------------------------------------------- + +CREATE TABLE IF NOT EXISTS cars ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + owner_id TEXT NOT NULL DEFAULT 'system', + visibility TEXT NOT NULL DEFAULT 'system' CHECK (visibility IN ('system', 'public', 'private')), + scale INT NOT NULL DEFAULT 27, + + length_mm DOUBLE PRECISION NOT NULL, + width_mm DOUBLE PRECISION NOT NULL, + height_mm DOUBLE PRECISION NOT NULL DEFAULT 0, + weight_g DOUBLE PRECISION NOT NULL DEFAULT 0, + + chassis_model TEXT NOT NULL DEFAULT '', + chassis_material TEXT NOT NULL DEFAULT '', + chassis_printed BOOLEAN NOT NULL DEFAULT FALSE, + chassis_wheelbase_mm DOUBLE PRECISION NOT NULL DEFAULT 0, + chassis_track_mm DOUBLE PRECISION NOT NULL DEFAULT 0, + + motor_kind TEXT NOT NULL DEFAULT 'brushed', + motor_class TEXT NOT NULL DEFAULT '', + motor_kv INT NOT NULL DEFAULT 0, + motor_power_w DOUBLE PRECISION NOT NULL DEFAULT 0, + + battery_voltage_v DOUBLE PRECISION NOT NULL DEFAULT 0, + battery_capacity_mah INT NOT NULL DEFAULT 0, + battery_cells INT NOT NULL DEFAULT 0, + battery_chemistry TEXT NOT NULL DEFAULT 'li-po', + + drive TEXT NOT NULL DEFAULT '2WD' CHECK (drive IN ('2WD', '4WD')), + + top_speed_ms DOUBLE PRECISION NOT NULL DEFAULT 0, + color_hex TEXT NOT NULL DEFAULT '#ff6b1a', + avatar_url TEXT NOT NULL DEFAULT '', + active BOOLEAN NOT NULL DEFAULT TRUE, + + total_distance_m DOUBLE PRECISION NOT NULL DEFAULT 0, + total_races INT NOT NULL DEFAULT 0, + total_laps INT NOT NULL DEFAULT 0, + best_lap_ms BIGINT NOT NULL DEFAULT 0, + + created_ms BIGINT NOT NULL, + updated_ms BIGINT NOT NULL, + device_id INT +); + +CREATE INDEX IF NOT EXISTS cars_visibility_idx ON cars (visibility); +CREATE INDEX IF NOT EXISTS cars_owner_idx ON cars (owner_id); + +-- --------------------------------------------------------------------------- +-- Clans & Drivers +-- --------------------------------------------------------------------------- + +CREATE TABLE IF NOT EXISTS clans ( + id TEXT PRIMARY KEY, + tag TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + avatar_url TEXT NOT NULL DEFAULT '', + created_ms BIGINT NOT NULL, + updated_ms BIGINT NOT NULL +); + +CREATE INDEX IF NOT EXISTS clans_tag_idx ON clans (tag); + +CREATE TABLE IF NOT EXISTS drivers ( + id TEXT PRIMARY KEY, + nickname TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + avatar_url TEXT NOT NULL DEFAULT '', + clan_id TEXT REFERENCES clans(id) ON DELETE SET NULL, + created_ms BIGINT NOT NULL, + updated_ms BIGINT NOT NULL +); + +CREATE INDEX IF NOT EXISTS drivers_nickname_idx ON drivers (nickname); +CREATE INDEX IF NOT EXISTS drivers_clan_idx ON drivers (clan_id); + +-- --------------------------------------------------------------------------- +-- Races & Race Drivers +-- --------------------------------------------------------------------------- + +CREATE TABLE IF NOT EXISTS races ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + track_id TEXT NOT NULL DEFAULT '', + max_cars INT NOT NULL, + laps INT NOT NULL, + time_limit_s INT NOT NULL, + status TEXT NOT NULL DEFAULT 'lobby' CHECK (status IN ('lobby', 'countdown', 'racing', 'finished', 'cancelled')), + created_ms BIGINT NOT NULL, + started_ms BIGINT NOT NULL DEFAULT 0, + finished_ms BIGINT NOT NULL DEFAULT 0, + duration_ms BIGINT NOT NULL DEFAULT 0, + total_laps INT NOT NULL DEFAULT 0, + total_drivers INT NOT NULL DEFAULT 0, + updated_ms BIGINT NOT NULL DEFAULT 0 +); + +CREATE INDEX IF NOT EXISTS races_status_finished_idx ON races (status, finished_ms DESC, id DESC); +CREATE INDEX IF NOT EXISTS races_started_idx ON races (status, started_ms DESC, id DESC); +CREATE INDEX IF NOT EXISTS races_track_idx ON races (track_id); +CREATE INDEX IF NOT EXISTS races_status_idx ON races (status); + +CREATE TABLE IF NOT EXISTS race_drivers ( + race_id TEXT NOT NULL REFERENCES races(id) ON DELETE CASCADE, + driver_id TEXT NOT NULL, + slot INT NOT NULL DEFAULT 0, + joined_ms BIGINT NOT NULL, + total_time_ms BIGINT NOT NULL DEFAULT 0, + best_lap_ms BIGINT NOT NULL DEFAULT 0, + position INT, + PRIMARY KEY (race_id, driver_id) +); + +CREATE INDEX IF NOT EXISTS race_drivers_driver_idx ON race_drivers (driver_id); +CREATE INDEX IF NOT EXISTS race_drivers_podium_idx ON race_drivers (race_id, total_time_ms ASC, position ASC) WHERE total_time_ms > 0; + +-- --------------------------------------------------------------------------- +-- Race Plans & Queue +-- --------------------------------------------------------------------------- + +CREATE TABLE IF NOT EXISTS race_plans ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + track_id TEXT NOT NULL DEFAULT 'default', + max_cars INT NOT NULL, + laps INT NOT NULL DEFAULT 0, + time_limit_s INT NOT NULL DEFAULT 0, + start_at_ms BIGINT NOT NULL, + interval_s INT NOT NULL DEFAULT 0, -- 0 = single-shot + count INT NOT NULL DEFAULT 0, -- 0 = repeat forever + enabled BOOLEAN NOT NULL DEFAULT TRUE, + created_ms BIGINT NOT NULL, + updated_ms BIGINT NOT NULL, + next_fire_ms BIGINT NOT NULL, -- last materialised fire time + fires_done INT NOT NULL DEFAULT 0 +); + +CREATE INDEX IF NOT EXISTS race_plans_start_idx ON race_plans (start_at_ms ASC, id ASC); +CREATE INDEX IF NOT EXISTS race_plans_enabled_idx ON race_plans (enabled, next_fire_ms); + +CREATE TABLE IF NOT EXISTS race_queue ( + driver_id TEXT NOT NULL, + race_id TEXT NOT NULL, + plan_id TEXT NOT NULL DEFAULT '', + enqueued_ms BIGINT NOT NULL, + PRIMARY KEY (driver_id, race_id) +); + +CREATE INDEX IF NOT EXISTS race_queue_race_idx ON race_queue (race_id); +CREATE INDEX IF NOT EXISTS race_queue_driver_idx ON race_queue (driver_id, enqueued_ms); + +-- --------------------------------------------------------------------------- +-- Lobby Drivers +-- --------------------------------------------------------------------------- + +CREATE TABLE IF NOT EXISTS lobby_drivers ( + driver_id TEXT PRIMARY KEY, + name TEXT NOT NULL DEFAULT '', + nickname TEXT NOT NULL DEFAULT '', + avatar_url TEXT NOT NULL DEFAULT '', + clan_id TEXT NOT NULL DEFAULT '', + clan_tag TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL DEFAULT 'idle' CHECK (status IN ('idle', 'racing', 'offline')), + current_race_id TEXT NOT NULL DEFAULT '', + connected_ms BIGINT NOT NULL, + last_seen_ms BIGINT NOT NULL, + car_id TEXT +); + +CREATE INDEX IF NOT EXISTS lobby_drivers_status_idx ON lobby_drivers (status); + +-- --------------------------------------------------------------------------- +-- Track Calendar +-- --------------------------------------------------------------------------- + +CREATE TABLE IF NOT EXISTS track_calendar ( + id SERIAL PRIMARY KEY, + track_id TEXT NOT NULL REFERENCES tracks(id) ON DELETE CASCADE, + start_at TIMESTAMP WITH TIME ZONE NOT NULL, + end_at TIMESTAMP WITH TIME ZONE NOT NULL, + CHECK (start_at < end_at) +); + +CREATE INDEX IF NOT EXISTS track_calendar_dates_idx ON track_calendar (start_at, end_at); + +-- --------------------------------------------------------------------------- +-- Go Migration Metadata (Bridges Go server startup migrations and Supabase) +-- --------------------------------------------------------------------------- + +CREATE TABLE IF NOT EXISTS schema_migrations ( + version TEXT PRIMARY KEY, + applied_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +INSERT INTO schema_migrations (version) VALUES + ('001_init.sql'), + ('002_seed.sql'), + ('003_f1_seed.sql'), + ('004_monaco_rescale.sql'), + ('005_races.sql'), + ('006_drop_host.sql'), + ('007_podium.sql'), + ('008_drivers_clans.sql'), + ('009_live_persistence.sql'), + ('010_unify_races.sql'), + ('011_drop_driver_ids.sql'), + ('012_normalize_race_results.sql'), + ('013_driver_device.sql'), + ('014_track_calendar.sql'), + ('015_driver_car_id.sql') +ON CONFLICT (version) DO NOTHING; + diff --git a/supabase/seed.sql b/supabase/seed.sql new file mode 100644 index 0000000..267db76 --- /dev/null +++ b/supabase/seed.sql @@ -0,0 +1,345 @@ +-- seed.sql — Initial system catalog (tracks + cars) for Supabase. + +-- --------------------------------------------------------------------------- +-- Monaco (Rescaled) +-- --------------------------------------------------------------------------- + +INSERT INTO tracks (id, name, description, author_id, visibility, scale, + length_m, width_m, lane_width_m, surface, best_lap_ms, best_lap_holder, + bounds_min_x, bounds_min_y, bounds_max_x, bounds_max_y, + created_ms, updated_ms) +VALUES ('monaco', 'Monaco', 'Circuit de Monaco. Narrow street circuit, tight hairpins, no room for error.', 'system', 'system', 27, + 21.87, 6.46, 0.18, 'tile', 0, 'Verstappen', + -3.231, -2.684, 3.231, 2.684, + 1700000000000, 1700000000000) +ON CONFLICT (id) DO NOTHING; + +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 0, -3.2064, 1.0454, -1.4710, 0.632, -0.1988) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 1, -3.1387, 0.5857, -1.3926, 0.632, -0.1787) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 2, -3.0418, 0.1312, -1.3043, 0.632, -0.2544) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 3, -2.8946, -0.3084, -1.1557, 0.632, -0.4431) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 4, -2.6693, -0.7141, -0.8629, 0.632, -1.0875) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 5, -2.3517, -0.9426, -0.1919, 0.632, -1.2844) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 6, -1.8935, -0.8648, 0.1182, 0.632, -0.2237) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 7, -1.4302, -0.8332, 0.0660, 0.632, 0.0475) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 8, -0.9667, -0.8035, 0.0743, 0.632, 0.0756) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 9, -0.5052, -0.7644, -0.0045, 0.632, 0.0806) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 10, -0.0425, -0.8077, -0.0000, 0.632, -0.0425) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 11, 0.4197, -0.7644, 0.0351, 0.632, 0.3650) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 12, 0.8809, -0.7753, -0.3862, 0.632, 1.3887) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 13, 1.1985, -1.0811, -1.2440, 0.632, 1.4550) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 14, 1.1372, -1.5315, -1.5878, 0.632, -0.2144) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 15, 1.1839, -1.9468, -1.0284, 0.632, -1.2681) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 16, 1.5551, -2.2249, -0.6162, 0.632, -0.4763) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 17, 1.9414, -2.4832, -0.5226, 0.632, -0.1756) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 18, 2.3526, -2.6842, -0.4546, 0.632, -0.1494) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 19, 2.3526, -2.6842, 1.2476, 0.632, -0.0225) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 20, 2.4622, -2.3570, 1.2545, 0.632, 0.1775) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 21, 2.5976, -1.9356, 0.5751, 0.632, 34.1925) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 22, 2.6154, -2.2577, -1.0415, 0.632, -0.8400) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 23, 2.9239, -2.4933, 0.0053, 0.632, -3.1975) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 24, 3.2311, -2.2544, 1.4159, 0.632, -2.7456) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 25, 3.0259, -1.8399, 2.1127, 0.632, -0.6488) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 26, 2.7546, -1.4629, 2.2528, 0.632, -0.2769) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 27, 2.4413, -1.1199, 2.3708, 0.632, -0.3044) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 28, 2.0895, -0.8170, 2.5300, 0.632, -0.3825) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 29, 1.6852, -0.5897, 2.7255, 0.632, -0.4019) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 30, 1.2447, -0.4437, 2.9031, 0.632, -0.3538) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 31, 0.7862, -0.3711, 3.0547, 0.632, -0.3044) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 32, 0.3218, -0.3632, -3.0973, 0.632, -0.0975) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 33, -0.1396, -0.4121, -3.1387, 0.632, -0.0394) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 34, -0.5818, -0.3659, -3.0594, 0.632, -0.2375) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 35, -1.0290, -0.4854, -2.9319, 0.632, -0.0950) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 36, -1.4880, -0.5588, -2.9664, 0.632, -0.2500) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 37, -1.9441, -0.6474, 3.1052, 0.632, 0.8825) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 38, -2.3631, -0.5269, 2.4971, 0.632, 1.3600) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 39, -2.6181, -0.1409, 1.9547, 0.632, 0.9481) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 40, -2.7014, 0.3108, 1.5706, 0.632, 0.3788) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 41, -2.6179, 0.7633, 1.6025, 0.632, -0.3425) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 42, -2.7299, 1.2092, 1.8788, 0.632, -0.0725) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 43, -2.8930, 1.6280, 1.6659, 0.632, 0.7706) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 44, -2.8134, 2.0840, 1.2045, 0.632, 0.2162) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 45, -2.5685, 2.4739, 1.5682, 0.632, -2.4475) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 46, -2.8118, 2.6842, -3.0638, 0.632, -4.0544) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 47, -3.1281, 2.4303, -2.0706, 0.632, -1.2500) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 48, -3.2004, 1.9728, -1.6822, 0.632, -0.4706) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 49, -3.2311, 1.5093, -1.5774, 0.632, -0.2275) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 50, -3.2064, 1.0454, -1.4710, 0.632, -0.1988) ON CONFLICT (track_id, seq) DO NOTHING; + +INSERT INTO track_tags (track_id, tag) VALUES ('monaco', 'f1') ON CONFLICT DO NOTHING; +INSERT INTO track_tags (track_id, tag) VALUES ('monaco', 'street') ON CONFLICT DO NOTHING; +INSERT INTO track_tags (track_id, tag) VALUES ('monaco', 'tight') ON CONFLICT DO NOTHING; +INSERT INTO track_tags (track_id, tag) VALUES ('monaco', 'slow') ON CONFLICT DO NOTHING; +INSERT INTO track_tags (track_id, tag) VALUES ('monaco', 'monaco') ON CONFLICT DO NOTHING; + +-- --------------------------------------------------------------------------- +-- Barcelona-Catalunya +-- --------------------------------------------------------------------------- + +INSERT INTO tracks (id, name, description, author_id, visibility, scale, + length_m, width_m, lane_width_m, surface, best_lap_ms, best_lap_holder, + bounds_min_x, bounds_min_y, bounds_max_x, bounds_max_y, + created_ms, updated_ms) +VALUES ('barcelona-catalunya', 'Barcelona-Catalunya', 'Circuit de Barcelona-Catalunya. Mix of high-speed corners and technical chicanes.', 'system', 'system', 27, + 3.75, 2.30, 0.18, 'tile', 0, 'Verstappen', + 0.3, -2.7, 4.05, -0.4, + 1700000000000, 1700000000000) +ON CONFLICT (id) DO NOTHING; + +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 0, 0.3, -0.4, -0, 4.5, 0) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 1, 1.4, -0.4, -0, 4.5, 0) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 2, 1.7, -0.65, -1.5707963267948966, 2.2, 1.6) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 3, 1.5, -0.95, -3.141592653589793, 2, 2) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 4, 1.2, -1.1, -3.4415926535897934, 2.6, 1) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 5, 0.95, -1, -4.0415926535897935, 2.6, 1) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 6, 0.8, -1.2, 1.5707963267948966, 2.4, 1.2) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 7, 1.05, -1.5, -0, 3.2, 0.7) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 8, 1.3, -1.65, -1.5707963267948966, 2.4, 1) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 9, 1.15, -1.85, -3.141592653589793, 2.4, 1) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 10, 1.4, -2, -2.641592653589793, 3, 0.7) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 11, 1.7, -2, -2.141592653589793, 3, 0.7) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 12, 1.95, -2.2, -1.5707963267948966, 2.8, 0.5) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 13, 1.65, -2.5, -0.3, 2.8, 0.5) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 14, 2.2, -2.7, -0.7, 3.2, 0.5) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 15, 2.8, -2.7, -1.1, 3.4, 0.5) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 16, 3.1, -2.5, -1.9707963267948967, 2.2, 1.4) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 17, 2.95, -2.2, -3.541592653589793, 2.2, 1.4) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 18, 3.3, -1.9, -0, 4, 0) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 19, 4, -1.9, -0, 4, 0) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 20, 4.05, -1.6, 1.5707963267948966, 2.4, 1.4) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 21, 3.8, -1.4, -3.141592653589793, 2.4, 1.4) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 22, 3.5, -1.5, -3.9269908169872414, 2.6, 1) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 23, 3.1, -1.25, 1.9707963267948967, 3, 0.7) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 24, 2.7, -1, 2.4707963267948965, 3, 0.7) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 25, 2.3, -0.7, -3.541592653589793, 3.4, 0.5) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 26, 1.6, -0.5, -3.3415926535897933, 3.6, 0.4) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 27, 0.9, -0.42, -0, 4, 0) ON CONFLICT (track_id, seq) DO NOTHING; + +INSERT INTO track_tags (track_id, tag) VALUES ('barcelona-catalunya', 'f1') ON CONFLICT DO NOTHING; +INSERT INTO track_tags (track_id, tag) VALUES ('barcelona-catalunya', 'balanced') ON CONFLICT DO NOTHING; +INSERT INTO track_tags (track_id, tag) VALUES ('barcelona-catalunya', 'testing') ON CONFLICT DO NOTHING; +INSERT INTO track_tags (track_id, tag) VALUES ('barcelona-catalunya', 'barcelona') ON CONFLICT DO NOTHING; + +-- --------------------------------------------------------------------------- +-- Austria (Red Bull Ring) +-- --------------------------------------------------------------------------- + +INSERT INTO tracks (id, name, description, author_id, visibility, scale, + length_m, width_m, lane_width_m, surface, best_lap_ms, best_lap_holder, + bounds_min_x, bounds_min_y, bounds_max_x, bounds_max_y, + created_ms, updated_ms) +VALUES ('austria-redbull-ring', 'Austria (Red Bull Ring)', 'Red Bull Ring. Short, fast, three straights separated by hairpins.', 'system', 'system', 27, + 3.55, 2.0, 0.18, 'wood', 0, 'Verstappen', + 0.3, -2.4, 3.85, -0.4, + 1700000000000, 1700000000000) +ON CONFLICT (id) DO NOTHING; + +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 0, 0.3, -0.4, -0, 4.8, 0) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 1, 1.6, -0.4, -0, 4.8, 0) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 2, 1.85, -0.65, -1.5707963267948966, 2, 1.8) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 3, 1.6, -0.95, -3.141592653589793, 2, 1.8) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 4, 1.3, -1, -3.641592653589793, 3, 0.7) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 5, 1.05, -1.2, 1.5707963267948966, 2.6, 1) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 6, 1.4, -1.4, -0, 3.2, 0.5) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 7, 1.85, -1.4, -1.7707963267948965, 3.2, 0.5) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 8, 2.1, -1.65, -3.141592653589793, 3, 0.7) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 9, 2, -1.85, 1.5707963267948966, 3, 0.7) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 10, 2.25, -2.05, -0, 2, 1.8) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 11, 2.4, -2, -1.5707963267948966, 2, 1.8) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 12, 2.4, -2.2, -3.141592653589793, 2, 1.8) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 13, 2.2, -2.3, 1.5707963267948966, 2, 1.8) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 14, 2.55, -2.4, 0.2, 3.4, 0.4) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 15, 3, -2.4, -0, 4, 0) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 16, 3.5, -2.4, -0, 4.5, 0) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 17, 3.85, -2.15, -1.7707963267948965, 3.4, 0.6) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 18, 3.6, -1.8, -3.141592653589793, 3, 0.8) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 19, 3.25, -1.85, -3.741592653589793, 3, 0.8) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 20, 3, -1.6, 1.9707963267948967, 3.2, 0.6) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 21, 2.6, -1.4, 3.141592653589793, 3.4, 0.5) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 22, 2, -1.2, -2.641592653589793, 3.6, 0.4) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 23, 1.4, -0.8, -2.9415926535897934, 3.8, 0.3) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 24, 0.8, -0.5, -0, 4.2, 0.2) ON CONFLICT (track_id, seq) DO NOTHING; + +INSERT INTO track_tags (track_id, tag) VALUES ('austria-redbull-ring', 'f1') ON CONFLICT DO NOTHING; +INSERT INTO track_tags (track_id, tag) VALUES ('austria-redbull-ring', 'short') ON CONFLICT DO NOTHING; +INSERT INTO track_tags (track_id, tag) VALUES ('austria-redbull-ring', 'fast') ON CONFLICT DO NOTHING; +INSERT INTO track_tags (track_id, tag) VALUES ('austria-redbull-ring', 'austria') ON CONFLICT DO NOTHING; + +-- --------------------------------------------------------------------------- +-- Great Britain (Silverstone) +-- --------------------------------------------------------------------------- + +INSERT INTO tracks (id, name, description, author_id, visibility, scale, + length_m, width_m, lane_width_m, surface, best_lap_ms, best_lap_holder, + bounds_min_x, bounds_min_y, bounds_max_x, bounds_max_y, + created_ms, updated_ms) +VALUES ('great-britain-silverstone', 'Great Britain (Silverstone)', 'Silverstone Circuit. Fast, flowing corners; home of British motorsport.', 'system', 'system', 27, + 3.55, 2.4, 0.18, 'carpet', 0, 'Hamilton', + 0.3, -2.65, 3.85, -0.25, + 1700000000000, 1700000000000) +ON CONFLICT (id) DO NOTHING; + +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 0, 0.3, -0.3, -0, 5, 0) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 1, 2.4, -0.3, -0, 5, 0) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 2, 2.8, -0.45, -1.5707963267948966, 3, 0.8) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 3, 2.6, -0.8, -3.141592653589793, 3.4, 0.6) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 4, 2.95, -1.05, -2.0707963267948966, 3.2, 0.7) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 5, 3.3, -1.3, -0, 2.2, 1.6) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 6, 3.6, -1.15, 1.5707963267948966, 2.2, 1.6) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 7, 3.6, -0.8, 3.141592653589793, 3, 0.7) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 8, 3.85, -0.55, 1.5707963267948966, 2.6, 1) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 9, 3.55, -0.25, -0, 2.4, 1) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 10, 3.25, -0.25, -1.5707963267948966, 2.4, 1) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 11, 3.05, -0.55, -1.8707963267948966, 3.4, 0.5) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 12, 2.85, -0.45, -3.141592653589793, 4, 0.3) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 13, 1.8, -0.3, -0, 4.6, 0) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 14, 1.2, -0.45, 1.7707963267948965, 3.8, 0.4) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 15, 1, -0.85, 3.541592653589793, 3.6, 0.5) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 16, 0.7, -1.15, -1.8707963267948966, 2.4, 1.2) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 17, 0.5, -0.9, -0, 2.6, 0.9) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 18, 0.7, -0.65, 1.5707963267948966, 3, 0.7) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 19, 0.45, -0.45, 3.541592653589793, 2.6, 1) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 20, 0.75, -1, -1.1707963267948966, 2.4, 1) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 21, 0.5, -1.5, 3.541592653589793, 2.6, 0.8) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 22, 0.3, -1.15, 1.5707963267948966, 2.4, 1) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 23, 0.7, -1.6, -0.7853981633974483, 3.4, 0.5) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 24, 2, -1.95, -1.9707963267948967, 3, 0.7) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 25, 3, -2.4, -3.3415926535897933, 3.4, 0.4) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 26, 2.4, -2.65, 1.8707963267948966, 4, 0.3) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 27, 1.4, -2.6, 3.141592653589793, 4.4, 0.2) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 28, 0.5, -2.3, 2.8415926535897933, 4.8, 0.2) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 29, 0.3, -1.7, 1.2707963267948965, 5, 0.1) ON CONFLICT (track_id, seq) DO NOTHING; + +INSERT INTO track_tags (track_id, tag) VALUES ('great-britain-silverstone', 'f1') ON CONFLICT DO NOTHING; +INSERT INTO track_tags (track_id, tag) VALUES ('great-britain-silverstone', 'fast') ON CONFLICT DO NOTHING; +INSERT INTO track_tags (track_id, tag) VALUES ('great-britain-silverstone', 'flowing') ON CONFLICT DO NOTHING; +INSERT INTO track_tags (track_id, tag) VALUES ('great-britain-silverstone', 'silverstone') ON CONFLICT DO NOTHING; + +-- --------------------------------------------------------------------------- +-- Belgium (Spa-Francorchamps) +-- --------------------------------------------------------------------------- + +INSERT INTO tracks (id, name, description, author_id, visibility, scale, + length_m, width_m, lane_width_m, surface, best_lap_ms, best_lap_holder, + bounds_min_x, bounds_min_y, bounds_max_x, bounds_max_y, + created_ms, updated_ms) +VALUES ('belgium-spa', 'Belgium (Spa-Francorchamps)', 'Spa-Francorchamps. Long, fast, legendary Eau Rouge / Raidillon.', 'system', 'system', 27, + 3.85, 2.25, 0.18, 'carpet', 0, 'Verstappen', + 0.4, -2.75, 4.25, -0.5, + 1700000000000, 1700000000000) +ON CONFLICT (id) DO NOTHING; + +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 0, 0.4, -0.5, -0, 4.8, 0) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 1, 1.6, -0.5, -0, 4.8, 0) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 2, 1.95, -0.6, -0.3, 4.2, 0.5) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 3, 2.2, -0.95, -0.9, 4, 0.6) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 4, 2.1, -1.3, -3.541592653589793, 4, 0.6) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 5, 2.7, -1.5, -0, 5, 0) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 6, 3.5, -1.5, -0, 5, 0) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 7, 3.85, -1.3, 1.5707963267948966, 2.4, 1.4) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 8, 3.6, -1.1, -3.4415926535897934, 2.6, 1.2) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 9, 3.3, -1.25, -4.0415926535897935, 2.6, 1.2) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 10, 3.5, -1.55, -1.5707963267948966, 3.6, 0.4) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 11, 3.25, -1.85, -0, 2.2, 1.6) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 12, 3.05, -1.7, 1.5707963267948966, 2.4, 1.4) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 13, 3.25, -1.5, -0, 3, 0.7) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 14, 3.7, -1.65, -1.8707963267948966, 3.4, 0.5) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 15, 4.1, -2, -2.1707963267948966, 4, 0.3) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 16, 4.25, -2.4, -3.541592653589793, 4.6, 0.3) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 17, 4, -2.7, 1.8707963267948966, 2.4, 1.4) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 18, 3.7, -2.55, -3.141592653589793, 2.4, 1.4) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 19, 3.55, -2.75, -3.8415926535897933, 2.4, 1.4) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 20, 3.05, -2.7, 1.5707963267948966, 2, 1.8) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 21, 2.85, -2.45, -0, 2, 1.8) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 22, 2, -2.2, -2.8415926535897933, 3.6, 0.3) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 23, 1.2, -1.6, -2.541592653589793, 4, 0.2) ON CONFLICT (track_id, seq) DO NOTHING; +INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 24, 0.6, -1.05, -2.8415926535897933, 4.4, 0.2) ON CONFLICT (track_id, seq) DO NOTHING; + +INSERT INTO track_tags (track_id, tag) VALUES ('belgium-spa', 'f1') ON CONFLICT DO NOTHING; +INSERT INTO track_tags (track_id, tag) VALUES ('belgium-spa', 'long') ON CONFLICT DO NOTHING; +INSERT INTO track_tags (track_id, tag) VALUES ('belgium-spa', 'fast') ON CONFLICT DO NOTHING; +INSERT INTO track_tags (track_id, tag) VALUES ('belgium-spa', 'spa') ON CONFLICT DO NOTHING; + +-- --------------------------------------------------------------------------- +-- Cars Catalog +-- --------------------------------------------------------------------------- + +INSERT INTO cars (id, name, owner_id, visibility, scale, + length_mm, width_mm, height_mm, weight_g, + chassis_model, chassis_material, chassis_printed, chassis_wheelbase_mm, chassis_track_mm, + motor_kind, motor_class, motor_kv, motor_power_w, + battery_voltage_v, battery_capacity_mah, battery_cells, battery_chemistry, + drive, top_speed_ms, color_hex, avatar_url, active, + created_ms, updated_ms) +VALUES ('f1-2024-redbull-rb20', 'Oracle Red Bull Racing RB20', 'system', 'system', 24, + 238, 83, 40, 33, + 'RB20 carbon monocoque', 'carbon-fibre', FALSE, 150, 75, + 'turbo-hybrid', 'F1 PU', 0, 746000, + 0, 0, 0, 'li-ion MGU-K', + '2WD', 95, '#1e3a8a', '', TRUE, + 1700000000000, 1700000000000) +ON CONFLICT (id) DO NOTHING; + +INSERT INTO cars (id, name, owner_id, visibility, scale, + length_mm, width_mm, height_mm, weight_g, + chassis_model, chassis_material, chassis_printed, chassis_wheelbase_mm, chassis_track_mm, + motor_kind, motor_class, motor_kv, motor_power_w, + battery_voltage_v, battery_capacity_mah, battery_cells, battery_chemistry, + drive, top_speed_ms, color_hex, avatar_url, active, + created_ms, updated_ms) +VALUES ('f1-2024-ferrari-sf24', 'Scuderia Ferrari SF-24', 'system', 'system', 24, + 238, 83, 40, 33, + 'SF-24 carbon monocoque', 'carbon-fibre', FALSE, 150, 75, + 'turbo-hybrid', 'F1 PU 066/12', 0, 746000, + 0, 0, 0, 'li-ion MGU-K', + '2WD', 94, '#dc2626', '', TRUE, + 1700000000000, 1700000000000) +ON CONFLICT (id) DO NOTHING; + +INSERT INTO cars (id, name, owner_id, visibility, scale, + length_mm, width_mm, height_mm, weight_g, + chassis_model, chassis_material, chassis_printed, chassis_wheelbase_mm, chassis_track_mm, + motor_kind, motor_class, motor_kv, motor_power_w, + battery_voltage_v, battery_capacity_mah, battery_cells, battery_chemistry, + drive, top_speed_ms, color_hex, avatar_url, active, + created_ms, updated_ms) +VALUES ('f1-2024-mclaren-mcl38', 'McLaren MCL38', 'system', 'system', 24, + 238, 83, 40, 33, + 'MCL38 carbon monocoque', 'carbon-fibre', FALSE, 150, 75, + 'turbo-hybrid', 'F1 PU Mercedes-derived', 0, 746000, + 0, 0, 0, 'li-ion MGU-K', + '2WD', 94, '#ea580c', '', TRUE, + 1700000000000, 1700000000000) +ON CONFLICT (id) DO NOTHING; + +INSERT INTO cars (id, name, owner_id, visibility, scale, + length_mm, width_mm, height_mm, weight_g, + chassis_model, chassis_material, chassis_printed, chassis_wheelbase_mm, chassis_track_mm, + motor_kind, motor_class, motor_kv, motor_power_w, + battery_voltage_v, battery_capacity_mah, battery_cells, battery_chemistry, + drive, top_speed_ms, color_hex, avatar_url, active, + created_ms, updated_ms) +VALUES ('f1-2024-mercedes-w15', 'Mercedes-AMG W15', 'system', 'system', 24, + 238, 83, 40, 33, + 'W15 carbon monocoque', 'carbon-fibre', FALSE, 150, 75, + 'turbo-hybrid', 'F1 PU M15', 0, 746000, + 0, 0, 0, 'li-ion MGU-K', + '2WD', 94, '#06b6d4', '', TRUE, + 1700000000000, 1700000000000) +ON CONFLICT (id) DO NOTHING; + +INSERT INTO cars (id, name, owner_id, visibility, scale, + length_mm, width_mm, height_mm, weight_g, + chassis_model, chassis_material, chassis_printed, chassis_wheelbase_mm, chassis_track_mm, + motor_kind, motor_class, motor_kv, motor_power_w, + battery_voltage_v, battery_capacity_mah, battery_cells, battery_chemistry, + drive, top_speed_ms, color_hex, avatar_url, active, + created_ms, updated_ms) +VALUES ('f1-2024-astonmartin-amr24', 'Aston Martin Aramco AMR24', 'system', 'system', 24, + 238, 83, 40, 33, + 'AMR24 carbon monocoque', 'carbon-fibre', FALSE, 150, 75, + 'turbo-hybrid', 'F1 PU Mercedes-derived', 0, 746000, + 0, 0, 0, 'li-ion MGU-K', + '2WD', 94, '#16a34a', '', TRUE, + 1700000000000, 1700000000000) +ON CONFLICT (id) DO NOTHING;