import type Database from 'better-sqlite3'; const initialized = new WeakSet(); export function ensureHostSchema(handle: Database.Database): void { if (initialized.has(handle)) return; handle.exec(` CREATE TABLE IF NOT EXISTS ec_hosts ( id TEXT PRIMARY KEY, name TEXT NOT NULL, hostname TEXT NOT NULL DEFAULT '', mode TEXT NOT NULL DEFAULT 'active' CHECK (mode IN ('active','draining','disabled')), port_min INTEGER NOT NULL DEFAULT 28960, port_max INTEGER NOT NULL DEFAULT 28980, bind_address TEXT NOT NULL DEFAULT '0.0.0.0', public_address TEXT NOT NULL DEFAULT '', minimum_free_bytes INTEGER NOT NULL DEFAULT 1073741824, created_at TEXT NOT NULL, enrolled_at TEXT, last_heartbeat TEXT, inventory TEXT NOT NULL DEFAULT '{}', deleted_at TEXT ); CREATE TABLE IF NOT EXISTS ec_host_enrollments ( id TEXT PRIMARY KEY, host_id TEXT NOT NULL REFERENCES ec_hosts(id), token_hash TEXT NOT NULL UNIQUE, created_at TEXT NOT NULL, expires_at TEXT NOT NULL, consumed_at TEXT ); CREATE TABLE IF NOT EXISTS ec_host_credentials ( id TEXT PRIMARY KEY, host_id TEXT NOT NULL REFERENCES ec_hosts(id), token_hash TEXT NOT NULL UNIQUE, created_at TEXT NOT NULL, revoked_at TEXT, last_used_at TEXT ); CREATE TABLE IF NOT EXISTS ec_host_operations ( id TEXT PRIMARY KEY, host_id TEXT NOT NULL REFERENCES ec_hosts(id), kind TEXT NOT NULL, instance_id TEXT, generation INTEGER, idempotency_key TEXT, payload TEXT NOT NULL, state TEXT NOT NULL DEFAULT 'pending' CHECK (state IN ('pending','accepted','running','completed','failed','unknown')), result TEXT, error TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, last_delivered_at TEXT, UNIQUE(host_id, idempotency_key) ); CREATE INDEX IF NOT EXISTS ec_host_operations_queue ON ec_host_operations(host_id, state, created_at); CREATE TABLE IF NOT EXISTS ec_host_operation_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, operation_id TEXT NOT NULL REFERENCES ec_host_operations(id), state TEXT NOT NULL, created_at TEXT NOT NULL, error TEXT ); CREATE TABLE IF NOT EXISTS ec_host_ports ( host_id TEXT NOT NULL REFERENCES ec_hosts(id), instance_id TEXT NOT NULL, bind_address TEXT NOT NULL, protocol TEXT NOT NULL CHECK (protocol IN ('udp','tcp')), port INTEGER NOT NULL CHECK (port BETWEEN 1 AND 65535), created_at TEXT NOT NULL, PRIMARY KEY(host_id,bind_address,protocol,port) ); CREATE INDEX IF NOT EXISTS ec_host_ports_instance ON ec_host_ports(instance_id); CREATE TABLE IF NOT EXISTS ec_host_decommissions ( host_id TEXT PRIMARY KEY REFERENCES ec_hosts(id), operation_id TEXT NOT NULL UNIQUE REFERENCES ec_host_operations(id), token_hash TEXT NOT NULL, stage TEXT NOT NULL DEFAULT 'queued' CHECK(stage IN ('queued','running','cleaned','failed','completed')), requested_at TEXT NOT NULL, updated_at TEXT NOT NULL, cleaned_at TEXT, error TEXT ); `); const columns = handle.prepare('PRAGMA table_info(ec_host_operations)').all() as { name: string }[]; if (!columns.some(column => column.name === 'last_delivered_at')) handle.exec('ALTER TABLE ec_host_operations ADD COLUMN last_delivered_at TEXT'); const hostColumns = handle.prepare('PRAGMA table_info(ec_hosts)').all() as { name: string }[]; if (!hostColumns.some(column => column.name === 'deleted_at')) handle.exec('ALTER TABLE ec_hosts ADD COLUMN deleted_at TEXT'); initialized.add(handle); }