import fs from 'node:fs'; import path from 'node:path'; import Database from 'better-sqlite3'; const SCHEMA = ` CREATE TABLE IF NOT EXISTS matches ( id TEXT PRIMARY KEY, team_a_name TEXT NOT NULL, team_b_name TEXT NOT NULL, mode TEXT NOT NULL, format TEXT NOT NULL, flow TEXT NOT NULL, maps TEXT NOT NULL, veto_start TEXT NOT NULL, first_team TEXT, status TEXT NOT NULL, organizer_token_hash TEXT NOT NULL, code_a TEXT NOT NULL UNIQUE, code_b TEXT NOT NULL UNIQUE, code_spectator TEXT NOT NULL UNIQUE, current_map_number INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, veto_started_at INTEGER, veto_finished_at INTEGER, finished_at INTEGER, last_activity_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS players ( id TEXT PRIMARY KEY, match_id TEXT NOT NULL REFERENCES matches(id) ON DELETE CASCADE, team TEXT NOT NULL CHECK (team IN ('A','B')), nickname TEXT NOT NULL, steam_id TEXT, token_hash TEXT NOT NULL UNIQUE, is_captain INTEGER NOT NULL DEFAULT 0, ready INTEGER NOT NULL DEFAULT 0, active INTEGER NOT NULL DEFAULT 1, joined_at INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS players_match ON players(match_id, team, active); CREATE TABLE IF NOT EXISTS veto_actions ( match_id TEXT NOT NULL REFERENCES matches(id) ON DELETE CASCADE, idx INTEGER NOT NULL, type TEXT NOT NULL, team TEXT, map TEXT, side TEXT, player_id TEXT, created_at INTEGER NOT NULL, PRIMARY KEY (match_id, idx) ); CREATE TABLE IF NOT EXISTS series_maps ( match_id TEXT NOT NULL REFERENCES matches(id) ON DELETE CASCADE, number INTEGER NOT NULL, map TEXT NOT NULL, picked_by TEXT, side_chosen_by TEXT, ct_team TEXT, status TEXT NOT NULL DEFAULT 'pending', PRIMARY KEY (match_id, number) ); -- Serverdaten inkl. Zugangsdaten. Diese Tabelle verlässt nie ungefiltert das Backend. CREATE TABLE IF NOT EXISTS servers ( match_id TEXT PRIMARY KEY REFERENCES matches(id) ON DELETE CASCADE, adapter TEXT NOT NULL, simulated INTEGER NOT NULL, status TEXT NOT NULL, message TEXT, handle TEXT, host TEXT, port INTEGER, tv_port INTEGER, rcon_password TEXT NOT NULL, server_password TEXT NOT NULL, tv_password TEXT NOT NULL, config_key TEXT NOT NULL, player_count INTEGER, attempts INTEGER NOT NULL DEFAULT 0, started_at INTEGER, ready_at INTEGER, configured_at INTEGER, stopped_at INTEGER, last_player_seen_at INTEGER, updated_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS events ( id INTEGER PRIMARY KEY AUTOINCREMENT, match_id TEXT NOT NULL REFERENCES matches(id) ON DELETE CASCADE, type TEXT NOT NULL, message TEXT NOT NULL, created_at INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS events_match ON events(match_id, id); `; export function openDatabase(file) { if (file !== ':memory:') fs.mkdirSync(path.dirname(file), { recursive: true }); const db = new Database(file); db.pragma('journal_mode = WAL'); db.pragma('foreign_keys = ON'); db.exec(SCHEMA); migrate(db); return db; } /** Ergänzt Spalten, die in älteren Datenbanken noch fehlen. */ function migrate(db) { const columns = (table) => new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((c) => c.name)); const seriesColumns = columns('series_maps'); const add = [ ['score_a', 'INTEGER NOT NULL DEFAULT 0'], ['score_b', 'INTEGER NOT NULL DEFAULT 0'], ['winner', 'TEXT'], // Live-Zustand aus den Serverlogs (JSON, siehe liveScore.js) ['live', 'TEXT'], ['live_updated_at', 'INTEGER'], ]; for (const [name, type] of add) { if (!seriesColumns.has(name)) db.exec(`ALTER TABLE series_maps ADD COLUMN ${name} ${type}`); } }