export async function migrate(sql: any) { await sql`CREATE EXTENSION IF NOT EXISTS pgcrypto`; // ── accounts ────────────────────────────────────────────────────────── await sql` CREATE TABLE IF NOT EXISTS accounts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), key TEXT NOT NULL UNIQUE, email_hash TEXT, plan TEXT NOT NULL DEFAULT 'free', plan_expires_at TIMESTAMPTZ, plan_stack JSONB NOT NULL DEFAULT '[]', created_at TIMESTAMPTZ DEFAULT now() ) `; // ── monitors ────────────────────────────────────────────────────────── 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, regions TEXT[] NOT NULL DEFAULT '{}', max_retries INTEGER NOT NULL DEFAULT 0, retry_interval_s INTEGER NOT NULL DEFAULT 30, resend_interval INTEGER NOT NULL DEFAULT 0, cert_alert_days INTEGER NOT NULL DEFAULT 0, max_redirects INTEGER NOT NULL DEFAULT 1, tags TEXT[] NOT NULL DEFAULT '{}', channel_ids UUID[] NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now() ) `; await sql`CREATE INDEX IF NOT EXISTS idx_monitors_account ON monitors(account_id)`; await sql`CREATE INDEX IF NOT EXISTS idx_monitors_tags ON monitors USING GIN(tags)`; await sql`CREATE INDEX IF NOT EXISTS idx_monitors_channel_ids ON monitors USING GIN(channel_ids)`; // ── pings ───────────────────────────────────────────────────────────── 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, important BOOLEAN NOT NULL DEFAULT false, error TEXT, meta JSONB, region TEXT NOT NULL DEFAULT 'default', run_id TEXT, cert_expiry_days INTEGER, cert_issuer TEXT, response_size INTEGER ) `; 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)`; // ── ping_bodies ─────────────────────────────────────────────────────── await sql` CREATE TABLE IF NOT EXISTS ping_bodies ( ping_id BIGINT PRIMARY KEY REFERENCES pings(id) ON DELETE CASCADE, body TEXT ) `; // ── api_keys ────────────────────────────────────────────────────────── 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`CREATE INDEX IF NOT EXISTS idx_api_keys_account ON api_keys(account_id)`; // ── monitor_region_state ────────────────────────────────────────────── 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) ) `; // ── notification_channels ───────────────────────────────────────────── 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)`; // ── status_pages ────────────────────────────────────────────────────── 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, bar_frequency TEXT NOT NULL DEFAULT 'daily', bar_count INTEGER NOT NULL DEFAULT 90, 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)`; // ── status_page_groups ──────────────────────────────────────────────── 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)`; // ── status_page_monitors ────────────────────────────────────────────── 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)`; // ── incidents ───────────────────────────────────────────────────────── 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)`; // ── incident_updates ────────────────────────────────────────────────── 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)`; // ── incident_monitors ───────────────────────────────────────────────── 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 INDEX IF NOT EXISTS idx_incident_monitors_monitor ON incident_monitors(monitor_id)`; // ── incident_status_pages ───────────────────────────────────────────── 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)`; // ── monitor_uptime_rollup ───────────────────────────────────────────── 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)`; // ── rollup_watermarks ───────────────────────────────────────────────── await sql` CREATE TABLE IF NOT EXISTS rollup_watermarks ( bucket_type TEXT PRIMARY KEY, last_aggregated_at TIMESTAMPTZ NOT NULL DEFAULT to_timestamp(0) ) `; // ── payments ────────────────────────────────────────────────────────── 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', amount_received TEXT NOT NULL DEFAULT '0', receipt_html TEXT, created_at TIMESTAMPTZ DEFAULT now(), paid_at TIMESTAMPTZ, expires_at TIMESTAMPTZ NOT NULL, txid TEXT ) `; 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`CREATE UNIQUE INDEX IF NOT EXISTS idx_payments_coin_derivation ON payments(coin, derivation_index)`; // ── payment_txs ─────────────────────────────────────────────────────── 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)`; console.log("DB ready"); }