Files
electric-cherry/src/lib/server/db/schema.sql
T

479 lines
24 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;
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_state TEXT, -- last observed Komodo runtime state
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: exactly one is non-null while a server is reachable. RCON
-- outages do NOT clear either — timers survive transient unreachability
-- and resume from the same anchor when the server comes back.
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, -- wall-clock ISO when we first saw a J; line for the current match cycle
game_ended_at TEXT, -- wall-clock ISO when we first saw ExitLevel/ShutdownGame end it
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);
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 REFERENCES servers(name),
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 REFERENCES servers(name),
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 REFERENCES servers(name),
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 REFERENCES servers(name),
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);
-- 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 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
);
-- 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,
action TEXT NOT NULL CHECK (action IN ('create', '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 '[]'
);
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
);
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 NOT NULL,
game_mode TEXT NOT NULL,
map_slug TEXT NOT NULL,
expected_map TEXT NOT NULL
);
-- Launch recipes copy managed files, not credentials from the key/integration stores.
-- Independent of the artifact library so history survives version deletion.
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
);
CREATE TABLE IF NOT EXISTS server_launch_recipes (
deployment_id TEXT PRIMARY KEY REFERENCES server_deployments(id) ON DELETE CASCADE,
recipe TEXT NOT NULL,
blueprint_name TEXT
);
CREATE TABLE IF NOT EXISTS server_launch_collections (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
created_at TEXT NOT NULL
);
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,
collection_id TEXT REFERENCES server_launch_collections(id) ON DELETE CASCADE
);
-- 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);
CREATE TRIGGER IF NOT EXISTS campaign_activity_file AFTER INSERT ON deployment_files
BEGIN
INSERT INTO campaign_events (deployment_id, message)
VALUES (NEW.deployment_id, NEW.server || ': ' || 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;