Courtside MVP: engine, web app, Docker + Cloudflare Tunnel deploy, docs
Tournament engine (pools, single/double elimination, standings with proper tiebreaks, withdrawals, two-court queue with ETAs and alerts), a single-process Node server with SQLite via node:sqlite and a WebSocket live board, organizer desk, QR landing page that follows the tournament phase, Dockerfile and compose with cloudflared, and documentation for deploying and running a day. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01MB7nCCAscYsb3zzkT6LHZi
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
// SQLite persistence using Node's built-in node:sqlite (Node 22.13+). No native modules.
|
||||
// Each tournament is stored as two JSON documents (tournament state + court engine state),
|
||||
// rewritten on every mutation. Data is tiny (a few hundred KB for a big day), so this is
|
||||
// simpler and more robust than a normalized schema, and a backup is one file copy.
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import { dirname } from 'node:path';
|
||||
|
||||
export class Store {
|
||||
constructor(path) {
|
||||
if (path !== ':memory:') mkdirSync(dirname(path), { recursive: true });
|
||||
this.db = new DatabaseSync(path);
|
||||
this.db.exec(`
|
||||
PRAGMA journal_mode = WAL;
|
||||
CREATE TABLE IF NOT EXISTS tournaments (
|
||||
slug TEXT PRIMARY KEY,
|
||||
tournament_json TEXT NOT NULL,
|
||||
engine_json TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
slug TEXT NOT NULL,
|
||||
ts INTEGER NOT NULL,
|
||||
actor TEXT,
|
||||
type TEXT NOT NULL,
|
||||
json TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS events_slug ON events(slug, id);
|
||||
`);
|
||||
this.stmts = {
|
||||
all: this.db.prepare('SELECT slug, tournament_json, engine_json FROM tournaments ORDER BY created_at DESC'),
|
||||
upsert: this.db.prepare(`INSERT INTO tournaments (slug, tournament_json, engine_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(slug) DO UPDATE SET tournament_json = excluded.tournament_json, engine_json = excluded.engine_json, updated_at = excluded.updated_at`),
|
||||
del: this.db.prepare('DELETE FROM tournaments WHERE slug = ?'),
|
||||
event: this.db.prepare('INSERT INTO events (slug, ts, actor, type, json) VALUES (?, ?, ?, ?, ?)'),
|
||||
events: this.db.prepare('SELECT ts, actor, type, json FROM events WHERE slug = ? ORDER BY id DESC LIMIT ?'),
|
||||
};
|
||||
}
|
||||
|
||||
loadAll() {
|
||||
return this.stmts.all.all().map(r => ({ slug: r.slug, tournament: JSON.parse(r.tournament_json), engine: r.engine_json ? JSON.parse(r.engine_json) : null }));
|
||||
}
|
||||
|
||||
save(slug, tournamentJson, engineJson) {
|
||||
const now = Date.now();
|
||||
this.stmts.upsert.run(slug, JSON.stringify(tournamentJson), engineJson ? JSON.stringify(engineJson) : null, tournamentJson.createdAt ?? now, now);
|
||||
}
|
||||
|
||||
remove(slug) { this.stmts.del.run(slug); }
|
||||
|
||||
logEvent(slug, evt) { this.stmts.event.run(slug, Date.now(), evt.actor ?? null, evt.type, JSON.stringify(evt)); }
|
||||
|
||||
events(slug, limit = 200) { return this.stmts.events.all(slug, limit).map(r => ({ ts: r.ts, ...JSON.parse(r.json) })); }
|
||||
|
||||
close() { this.db.close(); }
|
||||
}
|
||||
Reference in New Issue
Block a user