-- Monitoring + notification subsystem (see docs/monitoring-design.md). -- -- This migration is the consolidation of what were originally migrations -- 003_monitoring .. 010_notification_rules. Existing databases already recorded -- those versions in schema_migrations and have this schema, so they skip this -- file; only fresh databases run it (top to bottom). Columns that were once -- added by later ALTERs are folded into their CREATE TABLE definitions here. -- -- Standalone / project-free (SSH-broker model): monitor scope is 'global' -- (org-wide, admin) or 'personal' (owner-only, private). Numeric FKs internally, -- public_id at the API boundary. Sharing of global monitors is via -- monitor_targets; alert routing is via notification_rules + channels. -- --------------------------------------------------------------------------- -- Monitors + status + heartbeat history (orig. 003; facets from 004; cert -- fields from 006; cert_issuer_known from 009) -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS monitors ( id INTEGER PRIMARY KEY AUTOINCREMENT, public_id TEXT NOT NULL UNIQUE, scope TEXT NOT NULL DEFAULT 'global', owner_user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, name TEXT NOT NULL, type TEXT NOT NULL, target TEXT NOT NULL DEFAULT '', interval_sec INTEGER NOT NULL DEFAULT 60, timeout_sec INTEGER NOT NULL DEFAULT 10, enabled INTEGER NOT NULL DEFAULT 1, failure_threshold INTEGER NOT NULL DEFAULT 1, config TEXT NOT NULL DEFAULT '{}', created_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS monitor_status ( monitor_id INTEGER PRIMARY KEY REFERENCES monitors(id) ON DELETE CASCADE, state TEXT NOT NULL DEFAULT 'pending', running INTEGER NOT NULL DEFAULT 0, last_check_at INTEGER NOT NULL DEFAULT 0, next_check_at INTEGER NOT NULL DEFAULT 0, last_latency_ms INTEGER NOT NULL DEFAULT 0, last_error TEXT NOT NULL DEFAULT '', consecutive_failures INTEGER NOT NULL DEFAULT 0, last_state_change_at INTEGER NOT NULL DEFAULT 0, cert_expires_at INTEGER NOT NULL DEFAULT 0, cert_subject TEXT NOT NULL DEFAULT '', cert_issuer TEXT NOT NULL DEFAULT '', cert_sans TEXT NOT NULL DEFAULT '', cert_host_mismatch INTEGER NOT NULL DEFAULT 0, cert_issuer_known INTEGER NOT NULL DEFAULT 0, resolved_value TEXT NOT NULL DEFAULT '', updated_at INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS monitor_checks ( id INTEGER PRIMARY KEY AUTOINCREMENT, monitor_id INTEGER NOT NULL REFERENCES monitors(id) ON DELETE CASCADE, started_at INTEGER NOT NULL DEFAULT 0, finished_at INTEGER NOT NULL DEFAULT 0, ok INTEGER NOT NULL DEFAULT 0, state TEXT NOT NULL DEFAULT '', latency_ms INTEGER NOT NULL DEFAULT 0, message TEXT NOT NULL DEFAULT '', facets TEXT NOT NULL DEFAULT '[]' ); CREATE INDEX IF NOT EXISTS idx_monitors_scope_user ON monitors(scope, owner_user_id); CREATE INDEX IF NOT EXISTS idx_monitor_status_due ON monitor_status(running, next_check_at); CREATE INDEX IF NOT EXISTS idx_monitor_checks_monitor_started ON monitor_checks(monitor_id, started_at DESC); -- --------------------------------------------------------------------------- -- Principal-target sharing for global monitors (orig. 007) -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS monitor_targets ( monitor_id INTEGER NOT NULL REFERENCES monitors(id) ON DELETE CASCADE, principal_kind TEXT NOT NULL, -- 'user' | 'group' principal_id INTEGER NOT NULL, -- users(id) or user_groups(id) per kind access TEXT NOT NULL DEFAULT 'view', -- 'view' | 'manage' created_at INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (monitor_id, principal_kind, principal_id) ); CREATE INDEX IF NOT EXISTS idx_monitor_targets_principal ON monitor_targets(principal_kind, principal_id); -- --------------------------------------------------------------------------- -- Generic event / notification feed (orig. 005; notified_at outbox marker from -- 010). Monitoring is the first producer; other subsystems emit the same shape. -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS events ( id INTEGER PRIMARY KEY AUTOINCREMENT, public_id TEXT NOT NULL UNIQUE, kind TEXT NOT NULL, severity TEXT NOT NULL DEFAULT 'info', scope TEXT NOT NULL DEFAULT 'global', owner_user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, owner_project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE, source_type TEXT NOT NULL DEFAULT '', source_id TEXT NOT NULL DEFAULT '', title TEXT NOT NULL DEFAULT '', body TEXT NOT NULL DEFAULT '', payload TEXT NOT NULL DEFAULT '{}', dedup_key TEXT NOT NULL DEFAULT '', created_at INTEGER NOT NULL DEFAULT 0, notified_at INTEGER NOT NULL DEFAULT 0 ); -- Per-user read marker for the in-app feed. CREATE TABLE IF NOT EXISTS event_reads ( user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, last_read_at INTEGER NOT NULL DEFAULT 0 ); CREATE INDEX IF NOT EXISTS idx_events_created ON events(created_at DESC); CREATE INDEX IF NOT EXISTS idx_events_scope_project_created ON events(scope, owner_project_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_events_scope_user_created ON events(scope, owner_user_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_events_notified_at ON events(notified_at); -- --------------------------------------------------------------------------- -- Notification transports + channels + monitor attachment (orig. 008) -- Layer 1 transport_providers — the mechanism + server config (admin) -- Layer 3 notification_channels — named, scoped, selectable objects -- monitor_channels — monitor <-> channel (manager-assigned, m:n) -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS transport_providers ( id INTEGER PRIMARY KEY AUTOINCREMENT, public_id TEXT NOT NULL UNIQUE, name TEXT NOT NULL, type TEXT NOT NULL, -- 'webhook' | 'smtp' | 'microsoft_graph' | 'ms_teams' | 'slack' enabled INTEGER NOT NULL DEFAULT 1, config TEXT NOT NULL DEFAULT '{}', -- non-secret config JSON secret TEXT NOT NULL DEFAULT '', -- encrypted secret blob, never returned (json:"-") created_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS notification_channels ( id INTEGER PRIMARY KEY AUTOINCREMENT, public_id TEXT NOT NULL UNIQUE, name TEXT NOT NULL, scope TEXT NOT NULL DEFAULT 'global', -- 'global' | 'personal' owner_user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, enabled INTEGER NOT NULL DEFAULT 1, transport_provider_id INTEGER NOT NULL REFERENCES transport_providers(id) ON DELETE CASCADE, target_kind TEXT NOT NULL DEFAULT 'literal', -- 'literal' | 'user' | 'group' address TEXT NOT NULL DEFAULT '', -- literal destination principal_id INTEGER, -- users(id)/user_groups(id) when target_kind != 'literal' created_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0 ); CREATE INDEX IF NOT EXISTS idx_notification_channels_owner ON notification_channels(owner_user_id); CREATE TABLE IF NOT EXISTS monitor_channels ( monitor_id INTEGER NOT NULL REFERENCES monitors(id) ON DELETE CASCADE, channel_id INTEGER NOT NULL REFERENCES notification_channels(id) ON DELETE CASCADE, created_at INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (monitor_id, channel_id) ); CREATE INDEX IF NOT EXISTS idx_monitor_channels_channel ON monitor_channels(channel_id); -- --------------------------------------------------------------------------- -- Attribute-matched alert routing (orig. 010). Project/personal rules are OWNED -- by that principal and cascade-delete with it (real FKs). -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS notification_rules ( id INTEGER PRIMARY KEY AUTOINCREMENT, public_id TEXT NOT NULL UNIQUE, name TEXT NOT NULL, scope TEXT NOT NULL DEFAULT 'global', -- 'global' | 'project' owner_project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE, enabled INTEGER NOT NULL DEFAULT 1, kinds TEXT NOT NULL DEFAULT '', -- comma-separated kinds/prefixes ('repo.mirror_failed', 'repo.*'); empty = all min_severity TEXT NOT NULL DEFAULT 'warning', -- 'info' | 'warning' | 'critical' created_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0 ); CREATE INDEX IF NOT EXISTS idx_notification_rules_scope ON notification_rules(scope); CREATE INDEX IF NOT EXISTS idx_notification_rules_owner_project ON notification_rules(owner_project_id); CREATE TABLE IF NOT EXISTS notification_rule_channels ( rule_id INTEGER NOT NULL REFERENCES notification_rules(id) ON DELETE CASCADE, channel_id INTEGER NOT NULL REFERENCES notification_channels(id) ON DELETE CASCADE, created_at INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (rule_id, channel_id) ); CREATE INDEX IF NOT EXISTS idx_notification_rule_channels_channel ON notification_rule_channels(channel_id);