mirror of
https://github.com/IS1DI/x0gp.git
synced 2026-07-17 04:57:55 +00:00
This commit captures the full server code accumulated across
several development sessions. It includes the following layers,
applied in roughly this order during development:
1. Base infrastructure
- cmd/poc-server HTTP+WebSocket server, /health, /stats
- internal/transport wire types (Envelope + all message types)
- internal/catalog, internal/realtime, internal/stats
- internal/storage/postgres with migrations 001-004
- .env, .env.example, docker-compose, Dockerfile, scripts/
2. Swagger documentation
- go get github.com/swaggo/http-swagger
- swag init produces docs/docs.go
- /swagger/index.html UI, /swagger/doc.json spec
- annotations on health/stats/ws and catalog handlers
3. Races API
- internal/races/{store,service,keyset,types}.go
- migration 005_races.sql: finished_races, race_plans, race_queue
- GET /api/races with keyset pagination, status filter
- GET /api/races/upcoming
- POST /api/races/queue/join, GET, DELETE
- POST/GET /api/races/plans, DELETE /{id}
- lobby.RaceMeta simplification (no host_id/host_name)
4. Races seeder
- internal/races/seed with deterministic generator
- --seed-races / --reset CLI flags in main.go
- 30 finished + 5 live + 5 plans + 4 queue entries
5. Drivers and clans
- internal/drivers, internal/clans (CRUD, validation)
- migration 008_drivers_clans.sql
- /api/clans and /api/drivers endpoints
- 3-letter uppercase nickname/tag validation
- lobby.DriverMeta: Nickname, AvatarURL, ClanID, ClanTag
6. Podium
- internal/transport.RacePodiumEntry
- lobby.SetDriverProfile for in-memory metadata sync
- migration 007_podium.sql (podium JSONB column)
- seeder populates top-3 per finished race
7. Live races persistence
- migration 009_live_persistence.sql: live_races,
live_race_drivers, lobby_drivers
- internal/races/live_store.go: LiveStore with write-side
mirror for lobby.Service mutations
- Service.collectLive and Upcoming read from Postgres
- main.go RestoreFromDB rehydrates lobby on startup
8. Unify live + finished into one races table
- migration 010_unify_races.sql: rename finished_races to
races, expand status CHECK, merge live rows
- PgStore now hosts both write paths; LiveStore is a
thin facade implementing lobby.Persistence
- seeder resetAll drops only finished/cancelled rows and
race_plans / race_queue / lobby_drivers
Each layer is consistent on its own; cross-layer changes are
visible in the file history. A future refactor may split this
commit into the per-stage boundaries listed above.
267 lines
5.3 KiB
CSS
267 lines
5.3 KiB
CSS
/* x0gp · PoC web client stylesheet
|
|
Минималистичный тёмный UI. Без зависимостей, без сборки. */
|
|
|
|
* { box-sizing: border-box; }
|
|
html, body { margin: 0; padding: 0; height: 100%; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
|
|
Ubuntu, Cantarell, "Helvetica Neue", Arial, sans-serif;
|
|
background: #0e1117;
|
|
color: #e6edf3;
|
|
font-size: 14px;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.topbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 18px;
|
|
padding: 10px 16px;
|
|
background: #161b22;
|
|
border-bottom: 1px solid #30363d;
|
|
}
|
|
.brand .logo {
|
|
font-weight: 700;
|
|
letter-spacing: 0.5px;
|
|
color: #58a6ff;
|
|
font-size: 18px;
|
|
}
|
|
.brand .tag {
|
|
margin-left: 8px;
|
|
padding: 2px 6px;
|
|
font-size: 11px;
|
|
background: #1f6feb;
|
|
color: #fff;
|
|
border-radius: 4px;
|
|
text-transform: uppercase;
|
|
}
|
|
.conn {
|
|
padding: 4px 10px;
|
|
border-radius: 4px;
|
|
background: #6e7681;
|
|
color: #fff;
|
|
font-size: 12px;
|
|
}
|
|
.conn.connected { background: #2ea043; }
|
|
.conn.connecting { background: #d29922; }
|
|
.conn.error { background: #f85149; }
|
|
|
|
.hud-top {
|
|
margin-left: auto;
|
|
display: flex;
|
|
gap: 14px;
|
|
font-size: 12px;
|
|
color: #8b949e;
|
|
}
|
|
.hud-top b { color: #e6edf3; font-weight: 600; }
|
|
|
|
.layout {
|
|
display: grid;
|
|
grid-template-columns: 1fr 360px;
|
|
gap: 16px;
|
|
padding: 16px;
|
|
align-items: start;
|
|
}
|
|
|
|
/* Track canvas */
|
|
.track-wrap {
|
|
background: #161b22;
|
|
border: 1px solid #30363d;
|
|
border-radius: 8px;
|
|
padding: 12px;
|
|
}
|
|
.track-wrap canvas {
|
|
width: 100%;
|
|
height: auto;
|
|
display: block;
|
|
background: #0a0d12;
|
|
border-radius: 4px;
|
|
image-rendering: pixelated;
|
|
}
|
|
.legend {
|
|
display: flex;
|
|
gap: 16px;
|
|
margin-top: 8px;
|
|
font-size: 12px;
|
|
color: #8b949e;
|
|
}
|
|
.legend i.dot {
|
|
display: inline-block;
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
margin-right: 4px;
|
|
vertical-align: middle;
|
|
}
|
|
.dot.me { background: #58a6ff; }
|
|
.dot.other { background: #f0883e; }
|
|
.dot.ghost { background: #d29922; }
|
|
|
|
/* Controls aside */
|
|
.controls { display: flex; flex-direction: column; gap: 12px; }
|
|
.card {
|
|
background: #161b22;
|
|
border: 1px solid #30363d;
|
|
border-radius: 8px;
|
|
padding: 12px;
|
|
}
|
|
.card h3 {
|
|
margin: 0 0 10px;
|
|
font-size: 13px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
color: #8b949e;
|
|
}
|
|
.card label {
|
|
display: block;
|
|
font-size: 12px;
|
|
color: #8b949e;
|
|
margin-bottom: 8px;
|
|
}
|
|
.card input[type=text] {
|
|
display: block;
|
|
width: 100%;
|
|
margin-top: 4px;
|
|
padding: 6px 8px;
|
|
background: #0e1117;
|
|
border: 1px solid #30363d;
|
|
border-radius: 4px;
|
|
color: #e6edf3;
|
|
font-family: inherit;
|
|
font-size: 13px;
|
|
}
|
|
.card input[type=text]:focus {
|
|
outline: none;
|
|
border-color: #58a6ff;
|
|
}
|
|
|
|
.btn-row {
|
|
display: flex;
|
|
gap: 6px;
|
|
margin-top: 8px;
|
|
}
|
|
.btn {
|
|
flex: 1;
|
|
padding: 6px 10px;
|
|
background: #21262d;
|
|
border: 1px solid #30363d;
|
|
border-radius: 4px;
|
|
color: #e6edf3;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
font-family: inherit;
|
|
}
|
|
.btn:hover:not(:disabled) { background: #30363d; }
|
|
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
.btn.primary { background: #238636; border-color: #2ea043; }
|
|
.btn.primary:hover:not(:disabled) { background: #2ea043; }
|
|
|
|
.hint {
|
|
margin: 0 0 8px;
|
|
font-size: 11px;
|
|
color: #8b949e;
|
|
}
|
|
|
|
/* Pedals / bars */
|
|
.pedals { display: flex; flex-direction: column; gap: 6px; }
|
|
.bar {
|
|
display: grid;
|
|
grid-template-columns: 60px 1fr 44px;
|
|
gap: 8px;
|
|
align-items: center;
|
|
font-size: 12px;
|
|
}
|
|
.bar-track {
|
|
position: relative;
|
|
height: 12px;
|
|
background: #0e1117;
|
|
border: 1px solid #30363d;
|
|
border-radius: 3px;
|
|
overflow: hidden;
|
|
}
|
|
.bar-fill {
|
|
position: absolute;
|
|
top: 0; bottom: 0;
|
|
left: 50%;
|
|
width: 0%;
|
|
transition: left 60ms linear, width 60ms linear;
|
|
}
|
|
.bar-fill.green { background: #2ea043; }
|
|
.bar-fill.red { background: #f85149; }
|
|
.bar-fill.blue { background: #58a6ff; }
|
|
.bar b {
|
|
text-align: right;
|
|
font-variant-numeric: tabular-nums;
|
|
color: #e6edf3;
|
|
font-size: 12px;
|
|
}
|
|
|
|
/* Steering wheel visual */
|
|
.wheel {
|
|
display: flex;
|
|
justify-content: center;
|
|
margin-top: 10px;
|
|
}
|
|
.wheel-disc {
|
|
width: 90px;
|
|
height: 90px;
|
|
border-radius: 50%;
|
|
background: radial-gradient(circle at center, #30363d 0%, #21262d 70%);
|
|
border: 2px solid #484f58;
|
|
position: relative;
|
|
transition: transform 80ms linear;
|
|
}
|
|
.wheel-disc::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 4px;
|
|
left: 50%;
|
|
width: 4px;
|
|
height: 14px;
|
|
background: #f0883e;
|
|
transform: translateX(-50%);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
/* Telemetry */
|
|
.tel {
|
|
display: grid;
|
|
grid-template-columns: 100px 1fr;
|
|
gap: 4px 12px;
|
|
margin: 0;
|
|
font-size: 12px;
|
|
}
|
|
.tel dt { color: #8b949e; }
|
|
.tel dd {
|
|
margin: 0;
|
|
font-variant-numeric: tabular-nums;
|
|
color: #e6edf3;
|
|
}
|
|
|
|
/* Log */
|
|
.log-card .log {
|
|
margin: 0;
|
|
padding: 8px;
|
|
background: #0a0d12;
|
|
border: 1px solid #30363d;
|
|
border-radius: 4px;
|
|
height: 160px;
|
|
overflow-y: auto;
|
|
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
|
|
"Liberation Mono", monospace;
|
|
font-size: 11px;
|
|
line-height: 1.45;
|
|
color: #c9d1d9;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
.log .l-info { color: #58a6ff; }
|
|
.log .l-warn { color: #d29922; }
|
|
.log .l-error { color: #f85149; }
|
|
.log .l-rx { color: #2ea043; }
|
|
.log .l-tx { color: #f0883e; }
|
|
.log .l-meta { color: #8b949e; }
|
|
|
|
@media (max-width: 900px) {
|
|
.layout { grid-template-columns: 1fr; }
|
|
} |