708 lines
33 KiB
SQL
708 lines
33 KiB
SQL
-- Electric Cherry schema. Idempotent — safe to run at every boot via migrate.ts.
|
|
-- The old betas/beta_scripts/beta_deploys tables are dropped in-code before this
|
|
-- runs (see dropLegacyBetaTables in db/index.ts); the new script-centric model
|
|
-- lives here.
|
|
|
|
PRAGMA journal_mode = WAL;
|
|
PRAGMA foreign_keys = ON;
|
|
|
|
-- Historical gameplay deliberately has no FK to servers: destroying a server
|
|
-- must not erase records. Each player's counters are high-water marks per match.
|
|
CREATE TABLE IF NOT EXISTS leaderboard_matches (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
server TEXT NOT NULL,
|
|
server_deleted_at TEXT,
|
|
game TEXT NOT NULL,
|
|
map TEXT NOT NULL,
|
|
map_key TEXT NOT NULL,
|
|
map_name TEXT NOT NULL,
|
|
started_at TEXT NOT NULL,
|
|
ended_at TEXT,
|
|
last_received_at TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'active', -- active | completed | interrupted
|
|
start_observed INTEGER NOT NULL DEFAULT 0,
|
|
start_players INTEGER,
|
|
highest_round INTEGER NOT NULL DEFAULT 0,
|
|
last_event TEXT NOT NULL,
|
|
last_fingerprint TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_leaderboard_server ON leaderboard_matches(server, id DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_leaderboard_started ON leaderboard_matches(started_at DESC, id DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_leaderboard_map ON leaderboard_matches(game, map_key);
|
|
|
|
CREATE TABLE IF NOT EXISTS leaderboard_players (
|
|
game TEXT NOT NULL,
|
|
guid TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
first_seen_at TEXT NOT NULL,
|
|
last_seen_at TEXT NOT NULL,
|
|
PRIMARY KEY (game, guid)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS leaderboard_match_players (
|
|
match_id INTEGER NOT NULL REFERENCES leaderboard_matches(id) ON DELETE CASCADE,
|
|
guid TEXT NOT NULL,
|
|
highest_round INTEGER NOT NULL DEFAULT 0,
|
|
rounds_survived INTEGER NOT NULL DEFAULT 0,
|
|
kills INTEGER NOT NULL DEFAULT 0,
|
|
headshots INTEGER NOT NULL DEFAULT 0,
|
|
downs INTEGER NOT NULL DEFAULT 0,
|
|
revives INTEGER NOT NULL DEFAULT 0,
|
|
best_score INTEGER NOT NULL DEFAULT 0,
|
|
last_round INTEGER NOT NULL DEFAULT 0,
|
|
present INTEGER NOT NULL DEFAULT 1,
|
|
has_stats INTEGER NOT NULL DEFAULT 0,
|
|
completed INTEGER NOT NULL DEFAULT 0,
|
|
first_seen_at TEXT NOT NULL,
|
|
last_seen_at TEXT NOT NULL,
|
|
first_observed_round INTEGER, -- null for legacy participation
|
|
present_at_start INTEGER, -- null for legacy participation; one only when first seen in game_start
|
|
PRIMARY KEY (match_id, guid)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_leaderboard_player_matches ON leaderboard_match_players(guid, match_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS servers (
|
|
name TEXT PRIMARY KEY,
|
|
komodo_type TEXT NOT NULL DEFAULT 'deployment', -- deployment|stack|container
|
|
komodo_name TEXT NOT NULL,
|
|
komodo_server TEXT,
|
|
container_checked_at TEXT,
|
|
container_state TEXT, -- last observed Komodo runtime state
|
|
desired_state TEXT NOT NULL DEFAULT 'running' CHECK (desired_state IN ('running','stopped')),
|
|
game_port INTEGER, -- UDP RCON port; lazy-filled from Komodo
|
|
rcon_host TEXT, -- optional per-server override of RCON_HOST
|
|
game TEXT, -- 't5' etc.; static per-server (no map rotation)
|
|
map TEXT, -- 'zombie_theater' etc.; static per-server
|
|
last_players INTEGER,
|
|
last_checked_at TEXT,
|
|
last_map TEXT, -- from RCON getstatus mapname
|
|
last_gametype TEXT, -- from RCON getstatus g_gametype
|
|
last_roster TEXT, -- JSON array of RconPlayer
|
|
last_seen_at TEXT, -- ISO ts of last successful RCON reply
|
|
last_error TEXT, -- last RCON failure reason (cleared on success)
|
|
last_reply_snippet TEXT, -- hex+ASCII of last bad-reply UDP payload
|
|
-- Provisioning fields — set when Electric Cherry creates the deployment.
|
|
game_mode TEXT, -- 'zombie' | 'mp' | 'sp' (currently zombie only)
|
|
map_slug TEXT, -- 'cosmodrome' — short slug used in server name
|
|
host_dir TEXT, -- absolute host path bind-mounted into the container
|
|
config_file TEXT, -- e.g. 'dedicated_sp.cfg' (PLUTAINER_CONFIG_FILE)
|
|
game_id TEXT, -- e.g. 't5sp' (PLUTAINER_GAME)
|
|
image TEXT, -- e.g. 'ghcr.io/ayymoss/plutainer:latest'
|
|
created_by TEXT, -- 'ui' | operator identifier
|
|
destroyed_at TEXT, -- ISO ts when tombstoned (row usually deleted; kept as safety net)
|
|
-- Occupancy + match timers. active_since / inactive_since are mutually
|
|
-- exclusive. Accepted telemetry records transitions immediately; status
|
|
-- polls provide the RCON fallback. Outages do NOT clear either — timers
|
|
-- survive transient unreachability and resume from the same anchor.
|
|
active_since TEXT, -- ISO ts of last 0 -> >0 player transition
|
|
inactive_since TEXT, -- ISO ts of last >0 -> 0 player transition
|
|
game_started_at TEXT, -- receipt time of the script's game_start
|
|
game_ended_at TEXT, -- receipt time of the script's game_end
|
|
game_cycle_key TEXT, -- opaque marker for the currently-tracked cycle (the first J; line after the latest InitGame); lets us detect same-players game restarts
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
|
);
|
|
|
|
-- One row per artifact (a logical thing, e.g. "HUD tweaks"). Name is unique and
|
|
-- doubles as the slug in URLs. The `kind` column discriminates:
|
|
-- 'script' — GSC files deployed to runtime/plutonium/storage/t5/raw/scripts/sp
|
|
-- 'plugin' — .dll files deployed to runtime/plutonium/plugins
|
|
-- 'config' — cfg overlay (set K "V" lines) layered onto the baseline
|
|
-- 'baseline' — full dedicated_sp.cfg; exactly one per scope
|
|
-- Table + column names keep the "scripts" prefix for continuity; treat them
|
|
-- polymorphically at the app layer.
|
|
CREATE TABLE IF NOT EXISTS scripts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
target_rel_path TEXT NOT NULL, -- 'maps/mp_kino/hud.gsc' or 'hud.gsc'
|
|
scope_type TEXT, -- NULL | 'game' | 'map'
|
|
scope_value TEXT, -- NULL | 't5' | 'zombie_theater'
|
|
current_version_id INTEGER, -- FK → script_versions(id) after first promote
|
|
kind TEXT NOT NULL DEFAULT 'script', -- 'script' | 'config' | 'plugin' | 'baseline'
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_scripts_scope ON scripts(scope_type, scope_value);
|
|
-- kind-referencing indexes are created in db/index.ts AFTER migrateTable adds
|
|
-- the column to legacy DBs. Putting them here would fail with "no such column:
|
|
-- kind" on an existing pre-kind DB, breaking boot.
|
|
|
|
CREATE TABLE IF NOT EXISTS script_versions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
script_id INTEGER NOT NULL REFERENCES scripts(id) ON DELETE CASCADE,
|
|
version INTEGER NOT NULL,
|
|
release_version TEXT, -- publisher's version/tag, independent of upload order
|
|
sha256 TEXT NOT NULL,
|
|
size INTEGER NOT NULL,
|
|
content BLOB NOT NULL,
|
|
notes TEXT,
|
|
state TEXT NOT NULL DEFAULT 'untested', -- 'untested' | 'tested' | 'test-failed'
|
|
kind TEXT NOT NULL DEFAULT 'script', -- denormalized from scripts.kind for join-free filtering
|
|
uploaded_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
UNIQUE(script_id, version)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_script_versions_script ON script_versions(script_id);
|
|
|
|
-- Provenance belongs to the exact binary version, including when it is rolled back.
|
|
CREATE TABLE IF NOT EXISTS plugin_imports (
|
|
version_id INTEGER PRIMARY KEY REFERENCES script_versions(id) ON DELETE CASCADE,
|
|
repository TEXT NOT NULL,
|
|
release_id INTEGER NOT NULL,
|
|
release_url TEXT NOT NULL,
|
|
asset_id INTEGER NOT NULL,
|
|
asset_name TEXT NOT NULL,
|
|
published_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS server_scripts (
|
|
server TEXT NOT NULL REFERENCES servers(name) ON DELETE CASCADE,
|
|
rel_path TEXT NOT NULL,
|
|
script_version_id INTEGER REFERENCES script_versions(id) ON DELETE SET NULL,
|
|
sha256 TEXT NOT NULL,
|
|
size INTEGER NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
source TEXT, -- 'beta:<id>' | 'promote:<id>' | 'rollback:<id>'
|
|
PRIMARY KEY (server, rel_path)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_server_scripts_server ON server_scripts(server);
|
|
|
|
CREATE TABLE IF NOT EXISTS betas (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
-- DEPRECATED: multi-script betas are represented by beta_scripts. These
|
|
-- three columns are populated from the first bundled script to satisfy
|
|
-- NOT NULL on legacy schemas; the fleet code doesn't read them anymore.
|
|
script_id INTEGER NOT NULL REFERENCES scripts(id) ON DELETE CASCADE,
|
|
script_version_id INTEGER NOT NULL REFERENCES script_versions(id) ON DELETE CASCADE,
|
|
trigger_kind TEXT NOT NULL,
|
|
wait_for_health INTEGER NOT NULL DEFAULT 1,
|
|
wait_for_match INTEGER NOT NULL DEFAULT 0,
|
|
state TEXT NOT NULL, -- 'running' | 'ready-promote' | 'promoted' | 'rolled-back'
|
|
opened_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
closed_at TEXT,
|
|
notes TEXT
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_betas_state ON betas(state);
|
|
CREATE INDEX IF NOT EXISTS idx_betas_script ON betas(script_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS beta_servers (
|
|
beta_id INTEGER NOT NULL REFERENCES betas(id) ON DELETE CASCADE,
|
|
server TEXT NOT NULL,
|
|
server_deleted_at TEXT,
|
|
had_original INTEGER NOT NULL, -- deprecated; see beta_server_scripts
|
|
prior_version_id INTEGER REFERENCES script_versions(id), -- deprecated; see beta_server_scripts
|
|
stage TEXT NOT NULL, -- 'deploying'|'awaiting-empty'|'restarting'|'awaiting-health'|'awaiting-match'|'ready'|'failed'
|
|
error TEXT,
|
|
deployed_at TEXT,
|
|
restarted_at TEXT,
|
|
healthy_at TEXT,
|
|
match_seen_at TEXT,
|
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
PRIMARY KEY (beta_id, server)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_beta_servers_stage ON beta_servers(stage);
|
|
|
|
-- Immutable test configuration, durable attempts and append-only evidence.
|
|
-- Keeping test outcome separate preserves artifact holds until explicit closure.
|
|
CREATE TABLE IF NOT EXISTS beta_test_plans (
|
|
beta_id INTEGER PRIMARY KEY REFERENCES betas(id) ON DELETE CASCADE,
|
|
plan TEXT NOT NULL,
|
|
outcome TEXT NOT NULL DEFAULT 'active',
|
|
reason TEXT
|
|
);
|
|
CREATE TABLE IF NOT EXISTS beta_test_runs (
|
|
id TEXT PRIMARY KEY,
|
|
beta_id INTEGER NOT NULL REFERENCES betas(id) ON DELETE CASCADE,
|
|
server TEXT NOT NULL,
|
|
phase INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL,
|
|
observing_at TEXT,
|
|
retired_at TEXT,
|
|
metrics TEXT NOT NULL DEFAULT '{}',
|
|
cursors TEXT NOT NULL DEFAULT '{}'
|
|
);
|
|
CREATE TABLE IF NOT EXISTS beta_test_coverage (
|
|
beta_id INTEGER NOT NULL REFERENCES betas(id) ON DELETE CASCADE,
|
|
server TEXT NOT NULL,
|
|
game TEXT,
|
|
map TEXT,
|
|
PRIMARY KEY (beta_id, server)
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_beta_test_current_run ON beta_test_runs(beta_id, server) WHERE retired_at IS NULL;
|
|
CREATE TABLE IF NOT EXISTS beta_test_evidence (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
run_id TEXT NOT NULL REFERENCES beta_test_runs(id) ON DELETE CASCADE,
|
|
event_id TEXT NOT NULL,
|
|
received_at TEXT NOT NULL,
|
|
kind TEXT NOT NULL,
|
|
payload TEXT NOT NULL,
|
|
UNIQUE(run_id, event_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_beta_evidence_run ON beta_test_evidence(run_id, id);
|
|
CREATE TABLE IF NOT EXISTS beta_file_changes (
|
|
beta_id INTEGER NOT NULL REFERENCES betas(id) ON DELETE CASCADE,
|
|
server TEXT NOT NULL,
|
|
rel_path TEXT NOT NULL,
|
|
PRIMARY KEY (beta_id, server, rel_path)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS beta_test_presets (
|
|
name TEXT PRIMARY KEY,
|
|
plan TEXT NOT NULL
|
|
);
|
|
|
|
-- Many-to-many: which scripts (and pinned versions) are bundled into a beta.
|
|
-- Replaces the deprecated betas.script_id / betas.script_version_id columns.
|
|
CREATE TABLE IF NOT EXISTS beta_scripts (
|
|
beta_id INTEGER NOT NULL REFERENCES betas(id) ON DELETE CASCADE,
|
|
script_id INTEGER NOT NULL REFERENCES scripts(id) ON DELETE CASCADE,
|
|
script_version_id INTEGER NOT NULL REFERENCES script_versions(id) ON DELETE CASCADE,
|
|
kind TEXT NOT NULL DEFAULT 'script', -- denormalized from scripts.kind
|
|
PRIMARY KEY (beta_id, script_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_beta_scripts_beta ON beta_scripts(beta_id);
|
|
CREATE INDEX IF NOT EXISTS idx_beta_scripts_script ON beta_scripts(script_id);
|
|
|
|
-- Per (beta, server, script) prior-file state. A server row in beta_servers
|
|
-- covers restart progress; this table covers whether each script's target
|
|
-- existed on that server before the beta overwrote it — snapshot content
|
|
-- lives in beta_file_snapshots.
|
|
CREATE TABLE IF NOT EXISTS beta_server_scripts (
|
|
beta_id INTEGER NOT NULL REFERENCES betas(id) ON DELETE CASCADE,
|
|
server TEXT NOT NULL,
|
|
script_id INTEGER NOT NULL REFERENCES scripts(id) ON DELETE CASCADE,
|
|
had_original INTEGER NOT NULL,
|
|
prior_version_id INTEGER REFERENCES script_versions(id),
|
|
PRIMARY KEY (beta_id, server, script_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_beta_server_scripts_beta ON beta_server_scripts(beta_id);
|
|
|
|
-- Pre-beta bytes of every target file the beta overwrote on each server.
|
|
-- Absence of a row for (beta, server, rel_path) means the file did not exist
|
|
-- before the beta wrote it — rollback deletes the file rather than restoring.
|
|
-- Handles scripts, plugins, and the composed dedicated_sp.cfg uniformly.
|
|
CREATE TABLE IF NOT EXISTS beta_file_snapshots (
|
|
beta_id INTEGER NOT NULL REFERENCES betas(id) ON DELETE CASCADE,
|
|
server TEXT NOT NULL REFERENCES servers(name) ON DELETE CASCADE,
|
|
rel_path TEXT NOT NULL,
|
|
prior_content BLOB NOT NULL,
|
|
prior_sha256 TEXT NOT NULL,
|
|
prior_size INTEGER NOT NULL,
|
|
PRIMARY KEY (beta_id, server, rel_path)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_beta_file_snapshots_beta ON beta_file_snapshots(beta_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS deployments (
|
|
id TEXT PRIMARY KEY,
|
|
action TEXT NOT NULL, -- 'drift' | 'promote' | 'rollback'
|
|
started_at TEXT NOT NULL,
|
|
finished_at TEXT,
|
|
triggered_by TEXT,
|
|
script_id INTEGER REFERENCES scripts(id), -- promote/rollback only
|
|
script_version_id INTEGER REFERENCES script_versions(id), -- the version this deployment lands / restores
|
|
prior_version_id INTEGER REFERENCES script_versions(id), -- current_version_id before this promote
|
|
beta_id INTEGER REFERENCES betas(id), -- promote only
|
|
rolled_back_by TEXT REFERENCES deployments(id) -- set on the promote row when a rollback reverses it
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_deployments_started ON deployments(started_at DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS deployment_servers (
|
|
deployment_id TEXT NOT NULL REFERENCES deployments(id) ON DELETE CASCADE,
|
|
server TEXT NOT NULL, -- Campaign observation survives a rebuild's registry gap.
|
|
server_deleted_at TEXT,
|
|
files_changed INTEGER NOT NULL,
|
|
restart_state TEXT NOT NULL, -- pending | done | failed | manual | held
|
|
players INTEGER,
|
|
checked_at TEXT,
|
|
restarted_at TEXT,
|
|
attempts INTEGER NOT NULL DEFAULT 0,
|
|
error TEXT,
|
|
PRIMARY KEY (deployment_id, server)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_deployment_servers_state ON deployment_servers(restart_state);
|
|
|
|
-- Fleet-wide restart slots shared by deployments and betas. A successful API
|
|
-- response does not release a slot: the server must recover first. Persisting
|
|
-- these rows also protects the fleet across Electric Cherry process restarts.
|
|
CREATE TABLE IF NOT EXISTS restart_rollout (
|
|
server TEXT PRIMARY KEY REFERENCES servers(name) ON DELETE CASCADE,
|
|
requested_at TEXT NOT NULL,
|
|
checked_at TEXT,
|
|
healthy_at TEXT,
|
|
players INTEGER,
|
|
error TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS deployment_files (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
deployment_id TEXT NOT NULL REFERENCES deployments(id) ON DELETE CASCADE,
|
|
server TEXT NOT NULL,
|
|
verb TEXT NOT NULL, -- added | updated | removed
|
|
rel_path TEXT NOT NULL,
|
|
sha256_before TEXT,
|
|
sha256_after TEXT,
|
|
content_before BLOB,
|
|
content_after BLOB
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_deployment_files_dep ON deployment_files(deployment_id);
|
|
|
|
-- File payloads stay in SQLite until Provisioning has admitted their restart.
|
|
-- Existing campaigns/betas without rows here already wrote their files in the
|
|
-- previous release; they remain restart-only work during upgrade.
|
|
CREATE TABLE IF NOT EXISTS staged_file_changes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
source_kind TEXT NOT NULL CHECK (source_kind IN ('campaign','beta')),
|
|
source_id TEXT NOT NULL,
|
|
server TEXT NOT NULL,
|
|
kind TEXT NOT NULL,
|
|
rel_path TEXT NOT NULL,
|
|
content BLOB,
|
|
script_version_id INTEGER,
|
|
inventory_source TEXT NOT NULL,
|
|
deployment_file_id INTEGER REFERENCES deployment_files(id) ON DELETE CASCADE,
|
|
preserve_before_image INTEGER NOT NULL DEFAULT 0,
|
|
check_before INTEGER NOT NULL DEFAULT 0,
|
|
expected_before_sha TEXT,
|
|
state TEXT NOT NULL DEFAULT 'pending' CHECK (state IN ('pending','applying','applied','cancelled')),
|
|
applied_at TEXT,
|
|
UNIQUE (source_kind, source_id, server, kind, rel_path)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_staged_files_source ON staged_file_changes(source_kind, source_id, server);
|
|
CREATE TRIGGER IF NOT EXISTS staged_files_campaign_delete AFTER DELETE ON deployments BEGIN
|
|
DELETE FROM staged_file_changes WHERE source_kind = 'campaign' AND source_id = OLD.id;
|
|
END;
|
|
CREATE TRIGGER IF NOT EXISTS staged_files_beta_delete AFTER DELETE ON betas BEGIN
|
|
DELETE FROM staged_file_changes WHERE source_kind = 'beta' AND source_id = CAST(OLD.id AS TEXT);
|
|
END;
|
|
|
|
-- Per-script rows on aggregate promote/rollback deployments. For legacy
|
|
-- single-script deployments this may also be backfilled from the deprecated
|
|
-- deployments.script_id / script_version_id / prior_version_id columns.
|
|
CREATE TABLE IF NOT EXISTS deployment_scripts (
|
|
deployment_id TEXT NOT NULL REFERENCES deployments(id) ON DELETE CASCADE,
|
|
script_id INTEGER NOT NULL REFERENCES scripts(id) ON DELETE CASCADE,
|
|
script_version_id INTEGER NOT NULL REFERENCES script_versions(id) ON DELETE CASCADE,
|
|
prior_version_id INTEGER REFERENCES script_versions(id),
|
|
kind TEXT NOT NULL DEFAULT 'script', -- denormalized from scripts.kind
|
|
PRIMARY KEY (deployment_id, script_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_deployment_scripts_dep ON deployment_scripts(deployment_id);
|
|
|
|
-- Deliberately independent of servers: credentials survive server destruction.
|
|
CREATE TABLE IF NOT EXISTS server_keys (
|
|
server_id TEXT PRIMARY KEY,
|
|
pluto_key TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS plutonium_versions (
|
|
server TEXT PRIMARY KEY REFERENCES servers(name) ON DELETE CASCADE,
|
|
revision INTEGER,
|
|
checked_at TEXT NOT NULL,
|
|
error TEXT
|
|
);
|
|
CREATE TABLE IF NOT EXISTS maintenance_jobs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
server TEXT NOT NULL,
|
|
server_deleted_at TEXT,
|
|
state TEXT NOT NULL DEFAULT 'queued',
|
|
target_revision INTEGER,
|
|
requested_at TEXT NOT NULL,
|
|
restarted_at TEXT,
|
|
finished_at TEXT,
|
|
detail TEXT
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_maintenance_pending ON maintenance_jobs(server)
|
|
WHERE state IN ('queued', 'restarting');
|
|
CREATE TABLE IF NOT EXISTS server_telemetry (
|
|
server TEXT PRIMARY KEY REFERENCES servers(name) ON DELETE CASCADE,
|
|
round INTEGER NOT NULL,
|
|
players INTEGER NOT NULL,
|
|
received_at TEXT NOT NULL,
|
|
payload TEXT
|
|
);
|
|
-- Request diagnostics survive registry removal and also capture unknown names.
|
|
-- Bounded independently of the latest scoreboard and permanent leaderboard.
|
|
CREATE TABLE IF NOT EXISTS telemetry_requests (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
server TEXT NOT NULL,
|
|
received_at TEXT NOT NULL,
|
|
status INTEGER NOT NULL,
|
|
error TEXT,
|
|
event TEXT,
|
|
map TEXT,
|
|
round INTEGER,
|
|
player_count INTEGER,
|
|
content_type TEXT,
|
|
payload TEXT NOT NULL,
|
|
payload_truncated INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_telemetry_requests_server ON telemetry_requests(server, id DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_telemetry_requests_received ON telemetry_requests(received_at);
|
|
-- Lifecycle history deliberately survives removal of the server registry row.
|
|
CREATE TABLE IF NOT EXISTS server_deployments (
|
|
id TEXT PRIMARY KEY,
|
|
server TEXT NOT NULL,
|
|
server_deleted_at TEXT,
|
|
action TEXT NOT NULL CHECK (action IN ('create', 'start', 'restart', 'rebuild', 'stop', 'destroy')),
|
|
state TEXT NOT NULL,
|
|
stage TEXT NOT NULL,
|
|
started_at TEXT NOT NULL,
|
|
finished_at TEXT,
|
|
triggered_by TEXT NOT NULL,
|
|
error TEXT,
|
|
warnings TEXT NOT NULL DEFAULT '[]',
|
|
phase_key TEXT,
|
|
phase_started_at TEXT,
|
|
observed_at TEXT,
|
|
observation_json TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS server_deployment_events (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
deployment_id TEXT NOT NULL REFERENCES server_deployments(id) ON DELETE CASCADE,
|
|
created_at TEXT NOT NULL,
|
|
message TEXT NOT NULL,
|
|
kind TEXT,
|
|
phase_key TEXT,
|
|
details_json TEXT
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_server_deployment_events ON server_deployment_events(deployment_id, id);
|
|
|
|
CREATE TABLE IF NOT EXISTS server_creation_jobs (
|
|
deployment_id TEXT PRIMARY KEY REFERENCES server_deployments(id) ON DELETE CASCADE,
|
|
server TEXT NOT NULL,
|
|
port INTEGER,
|
|
game_mode TEXT NOT NULL,
|
|
map_slug TEXT NOT NULL,
|
|
expected_map TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS server_rebuild_jobs (
|
|
deployment_id TEXT PRIMARY KEY REFERENCES server_deployments(id) ON DELETE CASCADE,
|
|
removed_at TEXT
|
|
);
|
|
|
|
-- Stable links from consumers to lifecycle jobs. Sources survive their caller's
|
|
-- history retention, while current pointers live on the caller's server rows.
|
|
CREATE TABLE IF NOT EXISTS provisioning_sources (
|
|
operation_id TEXT NOT NULL REFERENCES server_deployments(id) ON DELETE CASCADE,
|
|
kind TEXT NOT NULL CHECK (kind IN ('campaign','beta','maintenance','watchdog')),
|
|
source_id TEXT NOT NULL,
|
|
server TEXT NOT NULL,
|
|
PRIMARY KEY (operation_id, kind, source_id, server)
|
|
);
|
|
|
|
-- Blueprints retain map/count selections only. The legacy column name remains
|
|
-- compatible with older snapshot-based definitions, read as selections at runtime.
|
|
CREATE TABLE IF NOT EXISTS server_blueprints (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL COLLATE NOCASE UNIQUE,
|
|
description TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL,
|
|
recipe TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS server_launch_previews (
|
|
id TEXT PRIMARY KEY,
|
|
expires_at TEXT NOT NULL,
|
|
server TEXT,
|
|
port INTEGER,
|
|
recipe TEXT NOT NULL,
|
|
blueprint_name TEXT,
|
|
deployment_id TEXT REFERENCES server_deployments(id) ON DELETE CASCADE
|
|
);
|
|
|
|
-- Queued plans contain metadata only. Attempted launch files survive library
|
|
-- deletion. New launches resolve current promotions instead of replaying these
|
|
-- snapshots; credentials stay in their stores.
|
|
CREATE TABLE IF NOT EXISTS server_launch_recipes (
|
|
deployment_id TEXT PRIMARY KEY REFERENCES server_deployments(id) ON DELETE CASCADE,
|
|
recipe TEXT NOT NULL,
|
|
recipe_summary TEXT,
|
|
blueprint_name TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS server_launch_collections (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
mode TEXT NOT NULL DEFAULT 'fill',
|
|
retained_servers TEXT NOT NULL DEFAULT '[]',
|
|
retained_server_deletions TEXT NOT NULL DEFAULT '{}'
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS server_collection_members (
|
|
collection_id TEXT NOT NULL REFERENCES server_launch_collections(id) ON DELETE CASCADE,
|
|
deployment_id TEXT NOT NULL UNIQUE REFERENCES server_deployments(id) ON DELETE CASCADE,
|
|
position INTEGER NOT NULL,
|
|
PRIMARY KEY (collection_id, position)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS server_collection_previews (
|
|
id TEXT PRIMARY KEY,
|
|
expires_at TEXT NOT NULL,
|
|
blueprint_name TEXT NOT NULL,
|
|
entries TEXT NOT NULL,
|
|
servers TEXT NOT NULL,
|
|
mode TEXT NOT NULL DEFAULT 'fill',
|
|
fleet_snapshot TEXT,
|
|
collection_id TEXT REFERENCES server_launch_collections(id) ON DELETE CASCADE
|
|
);
|
|
|
|
-- A pending removal can be shared by multiple reviewed collection requests.
|
|
CREATE TABLE IF NOT EXISTS server_collection_removals (
|
|
collection_id TEXT NOT NULL REFERENCES server_launch_collections(id) ON DELETE CASCADE,
|
|
deployment_id TEXT NOT NULL REFERENCES server_deployments(id) ON DELETE CASCADE,
|
|
position INTEGER NOT NULL,
|
|
PRIMARY KEY (collection_id, deployment_id)
|
|
);
|
|
|
|
-- Reusing a port is safe only after the reviewed removal has completed.
|
|
CREATE TABLE IF NOT EXISTS server_creation_dependencies (
|
|
deployment_id TEXT NOT NULL REFERENCES server_deployments(id) ON DELETE CASCADE,
|
|
removal_id TEXT NOT NULL REFERENCES server_deployments(id) ON DELETE CASCADE,
|
|
PRIMARY KEY (deployment_id, removal_id)
|
|
);
|
|
|
|
-- Persist state transitions atomically, including changes made by background workers.
|
|
-- Polls with unchanged status/error do not produce duplicate activity entries.
|
|
CREATE TABLE IF NOT EXISTS beta_events (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
beta_id INTEGER NOT NULL REFERENCES betas(id) ON DELETE CASCADE,
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
message TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_beta_events ON beta_events(beta_id, id);
|
|
CREATE TRIGGER IF NOT EXISTS beta_activity_open AFTER INSERT ON betas
|
|
BEGIN
|
|
INSERT INTO beta_events (beta_id, created_at, message)
|
|
VALUES (NEW.id, NEW.opened_at, 'Beta opened');
|
|
END;
|
|
|
|
CREATE TABLE IF NOT EXISTS campaign_events (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
deployment_id TEXT NOT NULL REFERENCES deployments(id) ON DELETE CASCADE,
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
message TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_campaign_events ON campaign_events(deployment_id, id);
|
|
DROP TRIGGER IF EXISTS campaign_activity_file;
|
|
CREATE TRIGGER campaign_activity_file AFTER INSERT ON deployment_files
|
|
BEGIN
|
|
INSERT INTO campaign_events (deployment_id, message)
|
|
VALUES (NEW.deployment_id, NEW.server || ': recorded file change — ' || NEW.verb || ' ' || NEW.rel_path);
|
|
END;
|
|
CREATE TRIGGER IF NOT EXISTS campaign_activity_open AFTER INSERT ON deployments
|
|
BEGIN
|
|
INSERT INTO campaign_events (deployment_id, created_at, message)
|
|
VALUES (NEW.id, NEW.started_at, 'Campaign started: ' || NEW.action);
|
|
END;
|
|
|
|
CREATE TRIGGER IF NOT EXISTS beta_activity_state AFTER UPDATE OF state ON betas
|
|
WHEN OLD.state IS NOT NEW.state
|
|
BEGIN
|
|
INSERT INTO beta_events (beta_id, message) VALUES (NEW.id, CASE NEW.state
|
|
WHEN 'ready-promote' THEN 'All beta servers are ready for review and promotion'
|
|
WHEN 'promoted' THEN 'Beta promoted'
|
|
WHEN 'rolled-back' THEN 'Beta rolled back'
|
|
ELSE 'Beta state: ' || NEW.state END);
|
|
END;
|
|
CREATE TRIGGER IF NOT EXISTS campaign_activity_finish AFTER UPDATE OF finished_at ON deployments
|
|
WHEN OLD.finished_at IS NULL AND NEW.finished_at IS NOT NULL
|
|
BEGIN
|
|
INSERT INTO campaign_events (deployment_id, created_at, message)
|
|
VALUES (NEW.id, NEW.finished_at, 'Campaign processing finished; see server statuses for held or manual work');
|
|
END;
|
|
|
|
CREATE TRIGGER IF NOT EXISTS beta_activity_server_insert AFTER INSERT ON beta_servers
|
|
|
|
BEGIN
|
|
INSERT INTO beta_events (beta_id, message) VALUES (NEW.beta_id, NEW.server || ': ' || CASE WHEN NEW.stage = 'deploying' THEN 'Deploying artifacts'
|
|
WHEN NEW.stage = 'awaiting-empty' THEN 'Waiting for an empty server'
|
|
WHEN NEW.stage = 'restarting' THEN 'Restarting server'
|
|
WHEN NEW.stage = 'awaiting-health' THEN 'Verifying health'
|
|
WHEN NEW.stage = 'awaiting-match' THEN 'Waiting for a completed match'
|
|
WHEN NEW.stage = 'ready' THEN 'Ready for promotion'
|
|
WHEN NEW.stage = 'failed' THEN 'Failed' ELSE NEW.stage END || CASE WHEN NEW.error IS NOT NULL THEN ' — ' || NEW.error ELSE '' END);
|
|
END;
|
|
|
|
CREATE TRIGGER IF NOT EXISTS beta_activity_server_update AFTER UPDATE ON beta_servers
|
|
WHEN OLD.stage IS NOT NEW.stage OR OLD.error IS NOT NEW.error OR OLD.restarted_at IS NOT NEW.restarted_at
|
|
BEGIN
|
|
INSERT INTO beta_events (beta_id, message) VALUES (NEW.beta_id, NEW.server || ': ' || CASE WHEN NEW.stage = 'deploying' THEN 'Deploying artifacts'
|
|
WHEN NEW.stage = 'awaiting-empty' THEN 'Waiting for an empty server'
|
|
WHEN NEW.stage = 'restarting' THEN 'Restarting server'
|
|
WHEN NEW.stage = 'awaiting-health' THEN 'Verifying health'
|
|
WHEN NEW.stage = 'awaiting-match' THEN 'Waiting for a completed match'
|
|
WHEN NEW.stage = 'ready' THEN 'Ready for promotion'
|
|
WHEN NEW.stage = 'failed' THEN 'Failed' ELSE NEW.stage END || CASE WHEN NEW.error IS NOT NULL THEN ' — ' || NEW.error ELSE '' END);
|
|
END;
|
|
|
|
CREATE TRIGGER IF NOT EXISTS campaign_activity_server_insert AFTER INSERT ON deployment_servers
|
|
|
|
BEGIN
|
|
INSERT INTO campaign_events (deployment_id, message) VALUES (NEW.deployment_id, NEW.server || ': ' || CASE WHEN NEW.restart_state = 'pending' AND NEW.restarted_at IS NOT NULL THEN 'Restart requested; verifying health' WHEN NEW.restart_state = 'pending' THEN 'Waiting for an empty server or restart capacity'
|
|
WHEN NEW.restart_state = 'done' THEN 'Server complete'
|
|
WHEN NEW.restart_state = 'failed' THEN 'Failed'
|
|
WHEN NEW.restart_state = 'manual' THEN 'Manual restart required'
|
|
WHEN NEW.restart_state = 'held' THEN 'Changes held' ELSE NEW.restart_state END || CASE WHEN NEW.error IS NOT NULL THEN ' — ' || NEW.error ELSE '' END);
|
|
END;
|
|
|
|
CREATE TRIGGER IF NOT EXISTS campaign_activity_server_update AFTER UPDATE ON deployment_servers
|
|
WHEN OLD.restart_state IS NOT NEW.restart_state OR OLD.error IS NOT NEW.error OR OLD.restarted_at IS NOT NEW.restarted_at
|
|
BEGIN
|
|
INSERT INTO campaign_events (deployment_id, message) VALUES (NEW.deployment_id, NEW.server || ': ' || CASE WHEN NEW.restart_state = 'pending' AND NEW.restarted_at IS NOT NULL THEN 'Restart requested; verifying health' WHEN NEW.restart_state = 'pending' THEN 'Waiting for an empty server or restart capacity'
|
|
WHEN NEW.restart_state = 'done' THEN 'Server complete'
|
|
WHEN NEW.restart_state = 'failed' THEN 'Failed'
|
|
WHEN NEW.restart_state = 'manual' THEN 'Manual restart required'
|
|
WHEN NEW.restart_state = 'held' THEN 'Changes held' ELSE NEW.restart_state END || CASE WHEN NEW.error IS NOT NULL THEN ' — ' || NEW.error ELSE '' END);
|
|
END;
|
|
-- Historical occupancy survives server removal; no player identities are stored.
|
|
CREATE TABLE IF NOT EXISTS metric_samples (
|
|
at INTEGER NOT NULL,
|
|
server TEXT NOT NULL,
|
|
game TEXT NOT NULL,
|
|
map TEXT NOT NULL,
|
|
players INTEGER,
|
|
capacity INTEGER,
|
|
PRIMARY KEY (at, server)
|
|
);
|
|
|
|
-- Watchdog history deliberately survives server destruction/rebuild.
|
|
CREATE TABLE IF NOT EXISTS watchdog_incidents (
|
|
id TEXT PRIMARY KEY,
|
|
server TEXT NOT NULL,
|
|
server_deleted_at TEXT,
|
|
kind TEXT NOT NULL CHECK (kind IN ('health','drift')),
|
|
state TEXT NOT NULL CHECK (state IN ('open','recovering','attention','resolved','dismissed')),
|
|
summary TEXT NOT NULL,
|
|
details TEXT NOT NULL DEFAULT '{}',
|
|
first_seen_at TEXT NOT NULL,
|
|
last_seen_at TEXT NOT NULL,
|
|
finished_at TEXT,
|
|
failures INTEGER NOT NULL DEFAULT 1,
|
|
cycle INTEGER NOT NULL DEFAULT 1
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_watchdog_active ON watchdog_incidents(server,kind) WHERE state != 'resolved';
|
|
CREATE TABLE IF NOT EXISTS watchdog_events (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
incident_id TEXT NOT NULL REFERENCES watchdog_incidents(id) ON DELETE CASCADE,
|
|
created_at TEXT NOT NULL,
|
|
message TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_watchdog_events ON watchdog_events(incident_id,id);
|
|
CREATE TABLE IF NOT EXISTS watchdog_actions (
|
|
id TEXT PRIMARY KEY,
|
|
incident_id TEXT NOT NULL REFERENCES watchdog_incidents(id) ON DELETE CASCADE,
|
|
action TEXT NOT NULL CHECK (action IN ('restart','rebuild')),
|
|
operation_id TEXT NOT NULL UNIQUE,
|
|
campaign_id TEXT,
|
|
created_at TEXT NOT NULL,
|
|
cycle INTEGER NOT NULL DEFAULT 1,
|
|
UNIQUE(incident_id,cycle,action)
|
|
);
|