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)`; 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"); }