CREATE TABLE IF NOT EXISTS platform_metadata ( singleton boolean PRIMARY KEY DEFAULT true CHECK(singleton), mode text NOT NULL CHECK(mode IN ('demo','production')) ); CREATE TABLE IF NOT EXISTS administrators ( id text PRIMARY KEY, username text NOT NULL UNIQUE, display_name text NOT NULL, role text NOT NULL CHECK (role IN ('super_admin','operator','viewer')), password_hash text NOT NULL ); ALTER TABLE administrators ADD COLUMN IF NOT EXISTS enabled boolean NOT NULL DEFAULT true; CREATE TABLE IF NOT EXISTS admin_sessions ( id text PRIMARY KEY, admin_id text NOT NULL REFERENCES administrators(id), expires_at timestamptz NOT NULL ); CREATE TABLE IF NOT EXISTS admin_session_details ( session_id text PRIMARY KEY REFERENCES admin_sessions(id) ON DELETE CASCADE, created_at timestamptz NOT NULL DEFAULT now(), client_ip text NOT NULL DEFAULT '', user_agent text NOT NULL DEFAULT '' ); CREATE TABLE IF NOT EXISTS admin_mfa ( admin_id text PRIMARY KEY REFERENCES administrators(id) ON DELETE CASCADE, encrypted_secret text NOT NULL DEFAULT '', last_counter bigint NOT NULL DEFAULT -1, pending_secret text NOT NULL DEFAULT '', pending_session text NOT NULL DEFAULT '', pending_expires timestamptz, enabled_at timestamptz ); CREATE TABLE IF NOT EXISTS admin_mfa_recovery ( admin_id text NOT NULL REFERENCES admin_mfa(admin_id) ON DELETE CASCADE, code_hash text NOT NULL, PRIMARY KEY(admin_id,code_hash) ); CREATE TABLE IF NOT EXISTS desktops ( id text PRIMARY KEY, name text NOT NULL, os text NOT NULL, node text NOT NULL, vmid integer NOT NULL, pool text NOT NULL DEFAULT '', power_state text NOT NULL, service_state text NOT NULL DEFAULT 'unknown', provider text NOT NULL CHECK (provider IN ('demo','proxmox')), endpoint text NOT NULL DEFAULT '', app text NOT NULL DEFAULT 'Desktop', width integer NOT NULL DEFAULT 2560, height integer NOT NULL DEFAULT 1440, fps integer NOT NULL DEFAULT 30, updated_at timestamptz NOT NULL DEFAULT now(), UNIQUE(provider,node,vmid) ); CREATE TABLE IF NOT EXISTS terminals ( id text PRIMARY KEY, sn text NOT NULL UNIQUE, name text NOT NULL, group_name text NOT NULL DEFAULT '', status text NOT NULL DEFAULT 'unregistered', agent_version text NOT NULL DEFAULT '', last_seen timestamptz, session_desktop_id text REFERENCES desktops(id), config_version integer NOT NULL DEFAULT 1, bootstrap_hash text NOT NULL, token_hash text UNIQUE, fps double precision, rtt double precision ); ALTER TABLE terminals ADD COLUMN IF NOT EXISTS enabled boolean NOT NULL DEFAULT true; CREATE TABLE IF NOT EXISTS users ( id text PRIMARY KEY, username text NOT NULL UNIQUE, display_name text NOT NULL, enabled boolean NOT NULL DEFAULT true, terminal_id text UNIQUE REFERENCES terminals(id) ); CREATE TABLE IF NOT EXISTS bindings ( user_id text NOT NULL REFERENCES users(id) ON DELETE CASCADE, desktop_id text NOT NULL REFERENCES desktops(id), PRIMARY KEY(user_id,desktop_id) ); CREATE TABLE IF NOT EXISTS resources ( id text PRIMARY KEY, name text NOT NULL, status text NOT NULL, cpu double precision NOT NULL, memory double precision NOT NULL, storage double precision NOT NULL, provider text NOT NULL, updated_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS tasks ( id text PRIMARY KEY, kind text NOT NULL, action text NOT NULL, target_id text NOT NULL, target_name text NOT NULL, actor text NOT NULL, status text NOT NULL, message text NOT NULL DEFAULT '', provider_ref text NOT NULL DEFAULT '', idempotency_key text NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), UNIQUE(actor,idempotency_key) ); CREATE UNIQUE INDEX IF NOT EXISTS tasks_active_target ON tasks(kind,target_id) WHERE status IN ('queued','running'); CREATE TABLE IF NOT EXISTS audit_logs ( id text PRIMARY KEY, actor text NOT NULL, action text NOT NULL, target text NOT NULL, result text NOT NULL, request_id text NOT NULL, created_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS connection_tickets ( token_hash text PRIMARY KEY, terminal_id text NOT NULL REFERENCES terminals(id), desktop_id text NOT NULL REFERENCES desktops(id), expires_at timestamptz NOT NULL, consumed_at timestamptz ); CREATE INDEX IF NOT EXISTS audit_created ON audit_logs(created_at DESC); CREATE INDEX IF NOT EXISTS tasks_created ON tasks(created_at DESC); ALTER TABLE terminals ADD COLUMN IF NOT EXISTS session_id text NOT NULL DEFAULT ''; ALTER TABLE terminals ADD COLUMN IF NOT EXISTS session_state text NOT NULL DEFAULT 'idle'; CREATE TABLE IF NOT EXISTS task_diagnostics ( task_id text PRIMARY KEY REFERENCES tasks(id) ON DELETE CASCADE, content jsonb NOT NULL, created_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS terminal_log_archives ( id text PRIMARY KEY, task_id text NOT NULL UNIQUE REFERENCES tasks(id) ON DELETE CASCADE, terminal_id text NOT NULL REFERENCES terminals(id) ON DELETE CASCADE, captured_at timestamptz NOT NULL DEFAULT now(), boot_id text NOT NULL DEFAULT '', agent_version text NOT NULL DEFAULT '', kiosk_state text NOT NULL DEFAULT '', log_text text NOT NULL DEFAULT '', metadata jsonb NOT NULL DEFAULT '{}'::jsonb ); CREATE INDEX IF NOT EXISTS terminal_log_archives_terminal_time ON terminal_log_archives(terminal_id,captured_at DESC,id); CREATE INDEX IF NOT EXISTS terminal_log_archives_time ON terminal_log_archives(captured_at DESC); CREATE TABLE IF NOT EXISTS image_templates ( id text PRIMARY KEY, name text NOT NULL, version text NOT NULL, os text NOT NULL CHECK(os IN ('Windows','Ubuntu','麒麟')), node text NOT NULL, vmid integer NOT NULL CHECK(vmid>=100), digest text NOT NULL, enabled boolean NOT NULL DEFAULT true, validated_at timestamptz NOT NULL DEFAULT now(), UNIQUE(node,vmid), UNIQUE(name,version) ); CREATE TABLE IF NOT EXISTS desktop_pools ( id text PRIMARY KEY, name text NOT NULL UNIQUE, type text NOT NULL CHECK(type IN ('dedicated','shared')), max_count integer NOT NULL CHECK(max_count BETWEEN 1 AND 10000), template_id text REFERENCES image_templates(id), enabled boolean NOT NULL DEFAULT true, created_at timestamptz NOT NULL DEFAULT now() ); ALTER TABLE desktops ADD COLUMN IF NOT EXISTS pool_id text REFERENCES desktop_pools(id); ALTER TABLE desktops ADD COLUMN IF NOT EXISTS setup_required boolean NOT NULL DEFAULT false; CREATE TABLE IF NOT EXISTS provisioning_jobs ( task_id text PRIMARY KEY REFERENCES tasks(id), template_id text NOT NULL REFERENCES image_templates(id), pool_id text REFERENCES desktop_pools(id), desktop_id text NOT NULL UNIQUE, vmid integer NOT NULL UNIQUE, request_json text NOT NULL, request_hash text NOT NULL, state text NOT NULL DEFAULT 'reserved' CHECK(state IN ('reserved','created','failed','uncertain')), created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS desktops_pool ON desktops(pool_id); CREATE TABLE IF NOT EXISTS import_batches ( id text PRIMARY KEY, actor_id text NOT NULL REFERENCES administrators(id), kind text NOT NULL, row_count integer NOT NULL, idempotency_key text NOT NULL, request_hash text NOT NULL, result jsonb NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), UNIQUE(actor_id,idempotency_key) ); CREATE TABLE IF NOT EXISTS session_history ( id text PRIMARY KEY, session_id text NOT NULL, terminal_id text NOT NULL REFERENCES terminals(id), terminal_name text NOT NULL, desktop_id text NOT NULL REFERENCES desktops(id), desktop_name text NOT NULL, username text NOT NULL DEFAULT '', display_name text NOT NULL DEFAULT '', state text NOT NULL CHECK(state IN ('connecting','connected','ended','interrupted')), source text NOT NULL CHECK(source IN ('ticket','observed')), reserved_at timestamptz NOT NULL DEFAULT now(), connected_at timestamptz, last_reported_at timestamptz NOT NULL DEFAULT now(), ended_at timestamptz, end_reason text NOT NULL DEFAULT '' ); CREATE UNIQUE INDEX IF NOT EXISTS session_history_active ON session_history(terminal_id) WHERE ended_at IS NULL; CREATE INDEX IF NOT EXISTS session_history_time ON session_history(reserved_at DESC,id); CREATE TABLE IF NOT EXISTS resource_samples ( resource_id text NOT NULL, sampled_at timestamptz NOT NULL DEFAULT now(), cpu double precision, memory double precision, storage double precision, PRIMARY KEY(resource_id,sampled_at) ); CREATE INDEX IF NOT EXISTS resource_samples_time ON resource_samples(sampled_at); CREATE TABLE IF NOT EXISTS alert_rules ( id text PRIMARY KEY, name text NOT NULL, metric text NOT NULL CHECK(metric IN ('node_cpu','node_memory','node_storage','node_unavailable','terminal_offline')), threshold double precision NOT NULL, hold_seconds integer NOT NULL DEFAULT 0, enabled boolean NOT NULL DEFAULT true ); ALTER TABLE alert_rules DROP CONSTRAINT IF EXISTS alert_rules_metric_check; ALTER TABLE alert_rules ADD CONSTRAINT alert_rules_metric_check CHECK(metric IN ('node_cpu','node_memory','node_storage','node_unavailable','terminal_offline','terminal_cpu','terminal_memory','terminal_temperature')); ALTER TABLE alert_rules ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now(); INSERT INTO alert_rules(id,name,metric,threshold,hold_seconds) VALUES ('rule_cpu','节点 CPU 高水位','node_cpu',90,120),('rule_memory','节点内存高水位','node_memory',90,120), ('rule_storage','节点存储高水位','node_storage',90,120),('rule_node','节点状态或采集异常','node_unavailable',120,0), ('rule_terminal','终端离线','terminal_offline',180,0) ON CONFLICT(id) DO NOTHING; CREATE TABLE IF NOT EXISTS alerts ( id text PRIMARY KEY, rule_id text NOT NULL REFERENCES alert_rules(id), rule_name text NOT NULL, metric text NOT NULL, target_id text NOT NULL, target_name text NOT NULL, value double precision NOT NULL, threshold double precision NOT NULL, status text NOT NULL DEFAULT 'firing', started_at timestamptz NOT NULL, opened_at timestamptz NOT NULL DEFAULT now(), resolved_at timestamptz, resolve_reason text NOT NULL DEFAULT '', acknowledged_at timestamptz, acknowledged_by text NOT NULL DEFAULT '' ); CREATE UNIQUE INDEX IF NOT EXISTS alerts_open ON alerts(rule_id,target_id) WHERE status='firing'; CREATE INDEX IF NOT EXISTS alerts_time ON alerts(opened_at DESC,id); CREATE TABLE IF NOT EXISTS alert_states ( rule_id text NOT NULL REFERENCES alert_rules(id), target_id text NOT NULL, first_seen timestamptz NOT NULL DEFAULT now(), PRIMARY KEY(rule_id,target_id) ); ALTER TABLE alert_states ADD COLUMN IF NOT EXISTS last_sample_at timestamptz; CREATE TABLE IF NOT EXISTS terminal_policy_templates ( id text PRIMARY KEY, name text NOT NULL, version text NOT NULL, heartbeat_interval integer NOT NULL CHECK(heartbeat_interval BETWEEN 15 AND 60), command_poll_interval integer NOT NULL CHECK(command_poll_interval BETWEEN 3 AND 30), desktop_refresh_interval integer NOT NULL CHECK(desktop_refresh_interval BETWEEN 5 AND 120), created_at timestamptz NOT NULL DEFAULT now(), UNIQUE(name,version) ); CREATE TABLE IF NOT EXISTS terminal_policy_capabilities ( terminal_id text PRIMARY KEY REFERENCES terminals(id) ON DELETE CASCADE, protocol integer NOT NULL DEFAULT 0 CHECK(protocol IN (0,1)) ); CREATE TABLE IF NOT EXISTS terminal_policy_deployments ( id text PRIMARY KEY, template_id text NOT NULL REFERENCES terminal_policy_templates(id), actor text NOT NULL, idempotency_key text NOT NULL, request_hash text NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), UNIQUE(actor,idempotency_key) ); CREATE TABLE IF NOT EXISTS terminal_policy_deliveries ( id text PRIMARY KEY, deployment_id text NOT NULL REFERENCES terminal_policy_deployments(id), terminal_id text NOT NULL REFERENCES terminals(id), config_version integer NOT NULL, status text NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','applied','failed','superseded')), error_code text NOT NULL DEFAULT '', acknowledged_at timestamptz, UNIQUE(terminal_id,config_version), UNIQUE(deployment_id,terminal_id) ); CREATE TABLE IF NOT EXISTS pve_connection ( singleton boolean PRIMARY KEY DEFAULT true CHECK(singleton), revision text NOT NULL, encrypted_config text NOT NULL, updated_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS login_limits ( key text PRIMARY KEY, window_started timestamptz NOT NULL DEFAULT now(), attempts integer NOT NULL ); CREATE INDEX IF NOT EXISTS login_limits_time ON login_limits(window_started); CREATE TABLE IF NOT EXISTS hardware_jobs ( task_id text PRIMARY KEY REFERENCES tasks(id), request_json text NOT NULL, request_hash text NOT NULL, stage text NOT NULL DEFAULT 'prepared' CHECK(stage IN ('prepared','submitting','completed','failed','uncertain')) ); CREATE UNIQUE INDEX IF NOT EXISTS tasks_compute_target ON tasks(target_id) WHERE kind IN ('desktop','hardware') AND status IN ('queued','running'); CREATE TABLE IF NOT EXISTS snapshot_jobs ( task_id text PRIMARY KEY REFERENCES tasks(id), request_json text NOT NULL, request_hash text NOT NULL, target_hash text NOT NULL, stage text NOT NULL DEFAULT 'prepared' CHECK(stage IN ('prepared','submitting','completed','failed','uncertain')) ); CREATE UNIQUE INDEX IF NOT EXISTS tasks_vm_maintenance_target ON tasks(target_id) WHERE kind IN ('desktop','hardware','snapshot') AND status IN ('queued','running'); CREATE TABLE IF NOT EXISTS power_batches ( id text PRIMARY KEY, actor text NOT NULL, action text NOT NULL CHECK(action IN ('start','shutdown','reboot')), idempotency_key text NOT NULL, request_hash text NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), UNIQUE(actor,idempotency_key) ); CREATE TABLE IF NOT EXISTS power_batch_items ( batch_id text NOT NULL REFERENCES power_batches(id), desktop_id text NOT NULL, desktop_name text NOT NULL, vmid integer NOT NULL, task_id text UNIQUE REFERENCES tasks(id), code text NOT NULL, message text NOT NULL, fingerprint text NOT NULL, stage text NOT NULL CHECK(stage IN ('skipped','prepared','submitting','completed','failed','uncertain')), PRIMARY KEY(batch_id,desktop_id) ); CREATE UNIQUE INDEX IF NOT EXISTS tasks_all_vm_actions_target ON tasks(target_id) WHERE kind IN ('desktop','hardware','snapshot','batch-desktop') AND status IN ('queued','running'); CREATE TABLE IF NOT EXISTS terminal_metrics_current ( terminal_id text PRIMARY KEY REFERENCES terminals(id) ON DELETE CASCADE, sampled_at timestamptz NOT NULL, supported boolean NOT NULL, cpu_percent double precision CHECK(cpu_percent BETWEEN 0 AND 100), memory_percent double precision CHECK(memory_percent BETWEEN 0 AND 100), soc_temperature_c double precision CHECK(soc_temperature_c BETWEEN -40 AND 150), uptime_seconds double precision CHECK(uptime_seconds BETWEEN 0 AND 3155760000) ); CREATE TABLE IF NOT EXISTS terminal_metric_samples ( terminal_id text NOT NULL REFERENCES terminals(id) ON DELETE CASCADE, sampled_at timestamptz NOT NULL, cpu_percent double precision CHECK(cpu_percent BETWEEN 0 AND 100), memory_percent double precision CHECK(memory_percent BETWEEN 0 AND 100), soc_temperature_c double precision CHECK(soc_temperature_c BETWEEN -40 AND 150), uptime_seconds double precision CHECK(uptime_seconds BETWEEN 0 AND 3155760000), PRIMARY KEY(terminal_id,sampled_at) ); CREATE INDEX IF NOT EXISTS terminal_metric_samples_expiry ON terminal_metric_samples(sampled_at);