feat(server): integrate UDP video service and normalize database schema

- Implement UDPVideoService to stream MJPEG video frames from ESP32s via UDP and send control commands.
- Normalize race database schema:
  - Remove redundant driver_ids array from races table (migration 011).
  - Move per-driver outcomes (total_time_ms, best_lap_ms, position) to race_drivers table (migration 012).
  - Add device_id column to lobby_drivers to link active physical devices (migration 013).
- Update WebSocket handler to pass lobbySvc, driversSvc, and videoSvc for driver identity tracking, syncing in-memory driver metadata, and forwarding WS controls to physical cars.
- Add CORS middleware to HTTP routes.
- Regenerate Swagger documentation.
This commit is contained in:
x0gp
2026-07-06 12:38:34 +04:00
parent 978d36c505
commit 49351b4928
19 changed files with 1406 additions and 132 deletions
@@ -0,0 +1,11 @@
-- 011_drop_driver_ids.sql — remove the driver_ids array column from
-- the unified races table.
--
-- The authoritative source for "which drivers are in this race" is
-- the race_drivers join table. The denormalised driver_ids TEXT[] was
-- redundant (and racy — the mirror goroutines could write one side
-- without the other). After this migration the only place where the
-- driver list lives is race_drivers, and the PgStore methods hydrate
-- it on read.
ALTER TABLE races DROP COLUMN IF EXISTS driver_ids;
@@ -0,0 +1,45 @@
-- 012_normalize_race_results.sql — move per-driver race outcomes
-- (total time, best lap, finishing position) from races into
-- race_drivers. The races table is left with only the metadata that
-- applies to the race as a whole; everything that depends on a
-- specific driver now lives in the join table.
--
-- Drop from races:
-- * podium (JSONB) — derived from race_drivers ordered by
-- total_time_ms ASC, top-3.
-- * winner_driver_id (TEXT) — first row by total_time_ms.
-- * winner_name (TEXT) — derived.
-- * best_lap_ms (BIGINT) — moved to race_drivers as the
-- MIN(best_lap_ms) over all rows in the race
-- (or stored on each row from the engine;
-- the aggregate is the same).
--
-- Add to race_drivers:
-- * total_time_ms BIGINT NOT NULL DEFAULT 0 (the driver's total race time)
-- * best_lap_ms BIGINT NOT NULL DEFAULT 0 (the driver's fastest lap)
-- * position INT (1..N, NULL when DNF/DSQ)
--
-- Note: race_drivers is shared by the live surface too. For live
-- races total_time_ms and best_lap_ms stay 0 (default) and position
-- stays NULL. The semantics of `position` is "final position when
-- the race finished"; the engine writes it on the finishing tick.
ALTER TABLE races DROP COLUMN IF EXISTS podium;
ALTER TABLE races DROP COLUMN IF EXISTS winner_driver_id;
ALTER TABLE races DROP COLUMN IF EXISTS winner_name;
ALTER TABLE races DROP COLUMN IF EXISTS best_lap_ms;
DROP INDEX IF EXISTS finished_races_podium_gin;
ALTER TABLE race_drivers
ADD COLUMN IF NOT EXISTS total_time_ms BIGINT NOT NULL DEFAULT 0;
ALTER TABLE race_drivers
ADD COLUMN IF NOT EXISTS best_lap_ms BIGINT NOT NULL DEFAULT 0;
ALTER TABLE race_drivers
ADD COLUMN IF NOT EXISTS position INT;
-- Keyset index over the join table for podium queries: "top-3 by
-- total_time_ms within this race" is the canonical podium lookup.
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;
@@ -0,0 +1,6 @@
-- 013_driver_device.sql — add device_id to lobby_drivers.
--
-- Represents the currently selected physical ESP32 DEVICE_ID (0..127) for a driver.
-- This device is pinned to the driver during races and released when the race ends or they leave.
ALTER TABLE lobby_drivers ADD COLUMN IF NOT EXISTS device_id INT;