Files
x0gp/server/web/style.css
T
x0gp 978d36c505 feat(server): initial implementation
This commit captures the full server code accumulated across
several development sessions. It includes the following layers,
applied in roughly this order during development:

1. Base infrastructure
   - cmd/poc-server HTTP+WebSocket server, /health, /stats
   - internal/transport wire types (Envelope + all message types)
   - internal/catalog, internal/realtime, internal/stats
   - internal/storage/postgres with migrations 001-004
   - .env, .env.example, docker-compose, Dockerfile, scripts/

2. Swagger documentation
   - go get github.com/swaggo/http-swagger
   - swag init produces docs/docs.go
   - /swagger/index.html UI, /swagger/doc.json spec
   - annotations on health/stats/ws and catalog handlers

3. Races API
   - internal/races/{store,service,keyset,types}.go
   - migration 005_races.sql: finished_races, race_plans, race_queue
   - GET /api/races with keyset pagination, status filter
   - GET /api/races/upcoming
   - POST /api/races/queue/join, GET, DELETE
   - POST/GET /api/races/plans, DELETE /{id}
   - lobby.RaceMeta simplification (no host_id/host_name)

4. Races seeder
   - internal/races/seed with deterministic generator
   - --seed-races / --reset CLI flags in main.go
   - 30 finished + 5 live + 5 plans + 4 queue entries

5. Drivers and clans
   - internal/drivers, internal/clans (CRUD, validation)
   - migration 008_drivers_clans.sql
   - /api/clans and /api/drivers endpoints
   - 3-letter uppercase nickname/tag validation
   - lobby.DriverMeta: Nickname, AvatarURL, ClanID, ClanTag

6. Podium
   - internal/transport.RacePodiumEntry
   - lobby.SetDriverProfile for in-memory metadata sync
   - migration 007_podium.sql (podium JSONB column)
   - seeder populates top-3 per finished race

7. Live races persistence
   - migration 009_live_persistence.sql: live_races,
     live_race_drivers, lobby_drivers
   - internal/races/live_store.go: LiveStore with write-side
     mirror for lobby.Service mutations
   - Service.collectLive and Upcoming read from Postgres
   - main.go RestoreFromDB rehydrates lobby on startup

8. Unify live + finished into one races table
   - migration 010_unify_races.sql: rename finished_races to
     races, expand status CHECK, merge live rows
   - PgStore now hosts both write paths; LiveStore is a
     thin facade implementing lobby.Persistence
   - seeder resetAll drops only finished/cancelled rows and
     race_plans / race_queue / lobby_drivers

Each layer is consistent on its own; cross-layer changes are
visible in the file history. A future refactor may split this
commit into the per-stage boundaries listed above.
2026-06-22 22:01:09 +04:00

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; }
}