export async function migrate(sql: any) { await sql`CREATE EXTENSION IF NOT EXISTS pgcrypto`; await sql` CREATE TABLE IF NOT EXISTS accounts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), key TEXT NOT NULL UNIQUE, email_hash TEXT, created_at TIMESTAMPTZ DEFAULT now() ) `; await sql` CREATE TABLE IF NOT EXISTS monitors ( id TEXT PRIMARY KEY DEFAULT encode(gen_random_bytes(8), 'hex'), account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, name TEXT NOT NULL, url TEXT NOT NULL, method TEXT NOT NULL DEFAULT 'GET', request_headers JSONB, request_body TEXT, timeout_ms INTEGER NOT NULL DEFAULT 30000, interval_s INTEGER NOT NULL DEFAULT 60, query JSONB, enabled BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ DEFAULT now() ) `; await sql` CREATE TABLE IF NOT EXISTS pings ( id BIGSERIAL PRIMARY KEY, monitor_id TEXT NOT NULL REFERENCES monitors(id) ON DELETE CASCADE, checked_at TIMESTAMPTZ NOT NULL DEFAULT now(), scheduled_at TIMESTAMPTZ, jitter_ms INTEGER, status_code INTEGER, latency_ms INTEGER, up BOOLEAN NOT NULL, error TEXT, meta JSONB ) `; await sql` CREATE TABLE IF NOT EXISTS ping_bodies ( ping_id BIGINT PRIMARY KEY REFERENCES pings(id) ON DELETE CASCADE, body TEXT ) `; await sql` CREATE TABLE IF NOT EXISTS api_keys ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), key TEXT NOT NULL UNIQUE, account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, label TEXT NOT NULL, created_at TIMESTAMPTZ DEFAULT now(), last_used_at TIMESTAMPTZ ) `; await sql`ALTER TABLE pings ADD COLUMN IF NOT EXISTS scheduled_at TIMESTAMPTZ`; await sql`ALTER TABLE pings ADD COLUMN IF NOT EXISTS jitter_ms INTEGER`; await sql`ALTER TABLE monitors ADD COLUMN IF NOT EXISTS regions TEXT[] NOT NULL DEFAULT '{}'`; await sql`ALTER TABLE pings ADD COLUMN IF NOT EXISTS region TEXT`; await sql`ALTER TABLE pings ADD COLUMN IF NOT EXISTS run_id TEXT`; await sql`ALTER TABLE accounts ADD COLUMN IF NOT EXISTS plan TEXT NOT NULL DEFAULT 'free'`; await sql`ALTER TABLE accounts ADD COLUMN IF NOT EXISTS plan_expires_at TIMESTAMPTZ`; await sql`ALTER TABLE accounts ADD COLUMN IF NOT EXISTS plan_stack JSONB NOT NULL DEFAULT '[]'`; await sql`ALTER TABLE monitors ADD COLUMN IF NOT EXISTS max_retries INTEGER NOT NULL DEFAULT 0`; await sql`ALTER TABLE monitors ADD COLUMN IF NOT EXISTS retry_interval_s INTEGER NOT NULL DEFAULT 30`; await sql`ALTER TABLE monitors ADD COLUMN IF NOT EXISTS resend_interval INTEGER NOT NULL DEFAULT 0`; await sql`ALTER TABLE monitors ADD COLUMN IF NOT EXISTS cert_alert_days INTEGER NOT NULL DEFAULT 0`; await sql`ALTER TABLE monitors ALTER COLUMN cert_alert_days SET DEFAULT 0`; await sql`ALTER TABLE pings ADD COLUMN IF NOT EXISTS important BOOLEAN NOT NULL DEFAULT false`; // Per-region transition state. region='' for unspecified/single-region monitors. await sql` CREATE TABLE IF NOT EXISTS monitor_region_state ( monitor_id TEXT NOT NULL REFERENCES monitors(id) ON DELETE CASCADE, region TEXT NOT NULL, last_state TEXT, consecutive_down INTEGER NOT NULL DEFAULT 0, cert_alert_sent BOOLEAN NOT NULL DEFAULT false, updated_at TIMESTAMPTZ DEFAULT now(), PRIMARY KEY (monitor_id, region) ) `; // Drop the now-stale per-monitor state columns from the previous slice. await sql`ALTER TABLE monitors DROP COLUMN IF EXISTS last_state`; await sql`ALTER TABLE monitors DROP COLUMN IF EXISTS consecutive_down`; // Notifications: modular providers, webhook only for now. await sql` CREATE TABLE IF NOT EXISTS notification_channels ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, name TEXT NOT NULL, kind TEXT NOT NULL, config JSONB NOT NULL, enabled BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ DEFAULT now() ) `; await sql`CREATE INDEX IF NOT EXISTS idx_notification_channels_account ON notification_channels(account_id)`; await sql` CREATE TABLE IF NOT EXISTS monitor_notifications ( monitor_id TEXT NOT NULL REFERENCES monitors(id) ON DELETE CASCADE, channel_id UUID NOT NULL REFERENCES notification_channels(id) ON DELETE CASCADE, PRIMARY KEY (monitor_id, channel_id) ) `; await sql`CREATE INDEX IF NOT EXISTS idx_monitor_notifications_channel ON monitor_notifications(channel_id)`; // Tier 3: monitor tags. One row per (monitor, tag). Used by the dashboard // home filter and the status page builder's "all monitors with tag X" picker. await sql` CREATE TABLE IF NOT EXISTS monitor_tags ( monitor_id TEXT NOT NULL REFERENCES monitors(id) ON DELETE CASCADE, tag TEXT NOT NULL, PRIMARY KEY (monitor_id, tag) ) `; await sql`CREATE INDEX IF NOT EXISTS idx_monitor_tags_tag ON monitor_tags(tag)`; // Tier 3: public status pages. The whole subgraph below is read by the // standalone apps/status service; writes happen via apps/api admin routes. await sql` CREATE TABLE IF NOT EXISTS status_pages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, slug TEXT NOT NULL UNIQUE, title TEXT NOT NULL, description TEXT, theme TEXT NOT NULL DEFAULT 'auto', password_hash TEXT, index_search BOOLEAN NOT NULL DEFAULT true, show_powered_by BOOLEAN NOT NULL DEFAULT true, show_response_time BOOLEAN NOT NULL DEFAULT true, show_cert_expiry BOOLEAN NOT NULL DEFAULT false, default_window TEXT NOT NULL DEFAULT '24h', custom_css TEXT, footer_text TEXT, og_image_url TEXT, analytics_html TEXT, auto_refresh_s INTEGER NOT NULL DEFAULT 60, created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now() ) `; await sql`CREATE INDEX IF NOT EXISTS idx_status_pages_account ON status_pages(account_id)`; // Display mode: 'compact' = one-line rows with click-to-expand details, // 'expanded' = full detail card always visible (default). await sql`ALTER TABLE status_pages ADD COLUMN IF NOT EXISTS display_mode TEXT NOT NULL DEFAULT 'expanded'`; await sql` CREATE TABLE IF NOT EXISTS status_page_groups ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), status_page_id UUID NOT NULL REFERENCES status_pages(id) ON DELETE CASCADE, name TEXT NOT NULL, position INTEGER NOT NULL DEFAULT 0 ) `; await sql`CREATE INDEX IF NOT EXISTS idx_status_page_groups_page ON status_page_groups(status_page_id)`; await sql` CREATE TABLE IF NOT EXISTS status_page_monitors ( status_page_id UUID NOT NULL REFERENCES status_pages(id) ON DELETE CASCADE, monitor_id TEXT NOT NULL REFERENCES monitors(id) ON DELETE CASCADE, group_id UUID REFERENCES status_page_groups(id) ON DELETE SET NULL, display_name TEXT, position INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (status_page_id, monitor_id) ) `; await sql`CREATE INDEX IF NOT EXISTS idx_status_page_monitors_monitor ON status_page_monitors(monitor_id)`; await sql` CREATE TABLE IF NOT EXISTS incidents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, title TEXT NOT NULL, status TEXT NOT NULL, severity TEXT NOT NULL DEFAULT 'minor', pinned BOOLEAN NOT NULL DEFAULT true, started_at TIMESTAMPTZ NOT NULL DEFAULT now(), resolved_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT now() ) `; await sql`CREATE INDEX IF NOT EXISTS idx_incidents_account ON incidents(account_id, started_at DESC)`; await sql` CREATE TABLE IF NOT EXISTS incident_updates ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), incident_id UUID NOT NULL REFERENCES incidents(id) ON DELETE CASCADE, status TEXT NOT NULL, body TEXT NOT NULL, body_html TEXT NOT NULL, created_at TIMESTAMPTZ DEFAULT now() ) `; await sql`CREATE INDEX IF NOT EXISTS idx_incident_updates_incident ON incident_updates(incident_id, created_at)`; await sql` CREATE TABLE IF NOT EXISTS incident_monitors ( incident_id UUID NOT NULL REFERENCES incidents(id) ON DELETE CASCADE, monitor_id TEXT NOT NULL REFERENCES monitors(id) ON DELETE CASCADE, PRIMARY KEY (incident_id, monitor_id) ) `; await sql` CREATE TABLE IF NOT EXISTS incident_status_pages ( incident_id UUID NOT NULL REFERENCES incidents(id) ON DELETE CASCADE, status_page_id UUID NOT NULL REFERENCES status_pages(id) ON DELETE CASCADE, PRIMARY KEY (incident_id, status_page_id) ) `; await sql`CREATE INDEX IF NOT EXISTS idx_incident_status_pages_page ON incident_status_pages(status_page_id)`; // Shared uptime rollup. One row per (monitor, region, bucket_type, bucket_start). // Powers status page uptime windows AND any future dashboard widgets. await sql` CREATE TABLE IF NOT EXISTS monitor_uptime_rollup ( monitor_id TEXT NOT NULL REFERENCES monitors(id) ON DELETE CASCADE, region TEXT NOT NULL, bucket_type TEXT NOT NULL, bucket_start TIMESTAMPTZ NOT NULL, total INTEGER NOT NULL, up_count INTEGER NOT NULL, avg_latency REAL, PRIMARY KEY (monitor_id, region, bucket_type, bucket_start) ) `; await sql`CREATE INDEX IF NOT EXISTS idx_uptime_rollup_lookup ON monitor_uptime_rollup(monitor_id, bucket_type, bucket_start DESC)`; await sql`CREATE INDEX IF NOT EXISTS idx_pings_monitor ON pings(monitor_id, checked_at DESC)`; await sql`CREATE INDEX IF NOT EXISTS idx_pings_checked_at ON pings(checked_at)`; await sql` CREATE TABLE IF NOT EXISTS payments ( id BIGSERIAL PRIMARY KEY, account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, plan TEXT NOT NULL, months INTEGER, amount_usd NUMERIC(10,2) NOT NULL, coin TEXT NOT NULL, amount_crypto TEXT NOT NULL, address TEXT NOT NULL, derivation_index INTEGER NOT NULL, status TEXT NOT NULL DEFAULT 'pending', created_at TIMESTAMPTZ DEFAULT now(), paid_at TIMESTAMPTZ, expires_at TIMESTAMPTZ NOT NULL, txid TEXT ) `; await sql`ALTER TABLE payments ADD COLUMN IF NOT EXISTS amount_received TEXT NOT NULL DEFAULT '0'`; await sql`ALTER TABLE payments ADD COLUMN IF NOT EXISTS receipt_html TEXT`; await sql` CREATE TABLE IF NOT EXISTS payment_txs ( id BIGSERIAL PRIMARY KEY, payment_id BIGINT NOT NULL REFERENCES payments(id) ON DELETE CASCADE, txid TEXT NOT NULL, amount TEXT NOT NULL, confirmed BOOLEAN NOT NULL DEFAULT false, detected_at TIMESTAMPTZ DEFAULT now(), UNIQUE(payment_id, txid) ) `; await sql`CREATE INDEX IF NOT EXISTS idx_payment_txs_payment ON payment_txs(payment_id)`; await sql`CREATE INDEX IF NOT EXISTS idx_payment_txs_txid ON payment_txs(txid)`; await sql`CREATE INDEX IF NOT EXISTS idx_payments_status ON payments(status)`; await sql`CREATE INDEX IF NOT EXISTS idx_payments_account ON payments(account_id)`; await sql`DROP INDEX IF EXISTS payments_derivation_index_key`; await sql`CREATE UNIQUE INDEX IF NOT EXISTS idx_payments_coin_derivation ON payments(coin, derivation_index)`; console.log("DB ready"); }