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
59 lines
2.6 KiB
JavaScript
59 lines
2.6 KiB
JavaScript
// 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(); }
|
|
}
|