From 29a9d6cea63d094ace1343093ecd0c6e848eea78 Mon Sep 17 00:00:00 2001 From: nate Date: Thu, 9 Apr 2026 19:18:04 +0400 Subject: [PATCH] refactor db --- apps/api/src/notifications/index.ts | 4 +- apps/api/src/routes/channels.ts | 5 + apps/api/src/routes/monitors.ts | 75 ++++++-------- apps/api/src/routes/pings.ts | 6 +- apps/pay/src/freedom.ts | 2 +- apps/shared/db.ts | 149 ++++++++++++---------------- apps/web/src/routes/dashboard.ts | 5 +- apps/web/src/views/detail.ejs | 2 +- 8 files changed, 109 insertions(+), 139 deletions(-) diff --git a/apps/api/src/notifications/index.ts b/apps/api/src/notifications/index.ts index b70166c..7dcbf4b 100644 --- a/apps/api/src/notifications/index.ts +++ b/apps/api/src/notifications/index.ts @@ -32,8 +32,8 @@ export async function dispatchForMonitor(monitorId: string, event: NotificationE const channels = await sql` SELECT c.id, c.account_id, c.name, c.kind, c.config, c.enabled FROM notification_channels c - JOIN monitor_notifications mn ON mn.channel_id = c.id - WHERE mn.monitor_id = ${monitorId} AND c.enabled = true + WHERE c.id = ANY((SELECT channel_ids FROM monitors WHERE id = ${monitorId})) + AND c.enabled = true `; if (channels.length === 0) return; await Promise.all(channels.map((c) => dispatch(c, event))); diff --git a/apps/api/src/routes/channels.ts b/apps/api/src/routes/channels.ts index 57f4fe0..6fb34ad 100644 --- a/apps/api/src/routes/channels.ts +++ b/apps/api/src/routes/channels.ts @@ -89,6 +89,11 @@ export const channels = new Elysia({ prefix: "/notifications/channels" }) RETURNING id `; if (!row) { set.status = 404; return { error: "Not found" }; } + // Remove this channel from any monitors that reference it. + await sql` + UPDATE monitors SET channel_ids = array_remove(channel_ids, ${params.id}::uuid) + WHERE account_id = ${accountId} AND ${params.id}::uuid = ANY(channel_ids) + `; return { deleted: true }; }, { detail: { summary: "Delete notification channel", tags: ["notifications"] } }) diff --git a/apps/api/src/routes/monitors.ts b/apps/api/src/routes/monitors.ts index 5e9ae0f..5e2f719 100644 --- a/apps/api/src/routes/monitors.ts +++ b/apps/api/src/routes/monitors.ts @@ -23,28 +23,18 @@ const MonitorBody = t.Object({ tags: t.Optional(t.Array(t.String({ pattern: "^[a-z0-9][a-z0-9-]{0,40}$" }), { description: "Lowercase tag slugs for grouping. Replaces the existing tag set." })), }); -async function replaceMonitorTags(monitorId: string, tags: string[]) { - await sql`DELETE FROM monitor_tags WHERE monitor_id = ${monitorId}`; - if (tags.length === 0) return; - const unique = Array.from(new Set(tags.map((t) => t.trim()).filter(Boolean))); - if (unique.length === 0) return; - const rows = unique.map((tag) => ({ monitor_id: monitorId, tag })); - await sql`INSERT INTO monitor_tags ${sql(rows, "monitor_id", "tag")}`; +function dedupeTags(tags: string[]): string[] { + return Array.from(new Set(tags.map((t) => t.trim()).filter(Boolean))); } -async function replaceMonitorChannels(monitorId: string, accountId: string, channelIds: string[]) { - await sql`DELETE FROM monitor_notifications WHERE monitor_id = ${monitorId}`; - if (channelIds.length === 0) return; - // Only attach channels that belong to the same account. Cast to uuid[] so the - // ANY() comparison against the uuid id column type-checks. +async function validateChannelIds(accountId: string, channelIds: string[]): Promise { + if (channelIds.length === 0) return []; const owned = await sql<{ id: string }[]>` SELECT id FROM notification_channels WHERE account_id = ${accountId} AND id = ANY(${sql.array(channelIds)}::uuid[]) `; - if (owned.length === 0) return; - const rows = owned.map((o) => ({ monitor_id: monitorId, channel_id: o.id })); - await sql`INSERT INTO monitor_notifications ${sql(rows, "monitor_id", "channel_id")}`; + return owned.map((o) => o.id); } export const monitors = new Elysia({ prefix: "/monitors" }) @@ -54,10 +44,9 @@ export const monitors = new Elysia({ prefix: "/monitors" }) const tag = (query as any)?.tag; if (tag) { return sql` - SELECT m.* FROM monitors m - JOIN monitor_tags mt ON mt.monitor_id = m.id - WHERE m.account_id = ${accountId} AND mt.tag = ${tag} - ORDER BY m.created_at DESC + SELECT * FROM monitors + WHERE account_id = ${accountId} AND ${tag} = ANY(tags) + ORDER BY created_at DESC `; } return sql`SELECT * FROM monitors WHERE account_id = ${accountId} ORDER BY created_at DESC`; @@ -92,8 +81,10 @@ export const monitors = new Elysia({ prefix: "/monitors" }) const ssrfError = await validateMonitorUrl(body.url); if (ssrfError) { set.status = 400; return { error: ssrfError }; } + const tags = body.tags ? dedupeTags(body.tags) : []; + const channelIds = body.channel_ids ? await validateChannelIds(accountId, body.channel_ids) : []; const [monitor] = await sql` - INSERT INTO monitors (account_id, name, url, method, request_headers, request_body, timeout_ms, interval_s, max_retries, retry_interval_s, resend_interval, cert_alert_days, query, regions) + INSERT INTO monitors (account_id, name, url, method, request_headers, request_body, timeout_ms, interval_s, max_retries, retry_interval_s, resend_interval, cert_alert_days, query, regions, tags, channel_ids) VALUES ( ${accountId}, ${body.name}, ${body.url}, ${(body.method ?? 'GET').toUpperCase()}, @@ -106,12 +97,12 @@ export const monitors = new Elysia({ prefix: "/monitors" }) ${body.resend_interval ?? 0}, ${body.cert_alert_days ?? 0}, ${body.query ? sql.json(body.query) : null}, - ${sql.array(regions)} + ${sql.array(regions)}, + ${sql.array(tags)}, + ${sql.array(channelIds)}::uuid[] ) RETURNING * `; - if (body.channel_ids) await replaceMonitorChannels(monitor.id, accountId, body.channel_ids); - if (body.tags) await replaceMonitorTags(monitor.id, body.tags); invalidateMonitorList(); return monitor; }, { body: MonitorBody, detail: { summary: "Create monitor", tags: ["monitors"] } }) @@ -126,13 +117,7 @@ export const monitors = new Elysia({ prefix: "/monitors" }) SELECT * FROM pings WHERE monitor_id = ${params.id} ORDER BY checked_at DESC LIMIT 100 `; - const channels = await sql<{ channel_id: string }[]>` - SELECT channel_id FROM monitor_notifications WHERE monitor_id = ${params.id} - `; - const tagRows = await sql<{ tag: string }[]>` - SELECT tag FROM monitor_tags WHERE monitor_id = ${params.id} ORDER BY tag - `; - return { ...monitor, results, channel_ids: channels.map((c) => c.channel_id), tags: tagRows.map((t) => t.tag) }; + return { ...monitor, results }; }, { detail: { summary: "Get monitor with results", tags: ["monitors"] } }) .patch("/:id", async ({ accountId, plan, params, body, set }) => { @@ -158,27 +143,29 @@ export const monitors = new Elysia({ prefix: "/monitors" }) if (ssrfError) { set.status = 400; return { error: ssrfError }; } } + const validatedChannelIds = body.channel_ids ? await validateChannelIds(accountId, body.channel_ids) : null; const [monitor] = await sql` UPDATE monitors SET - name = COALESCE(${body.name ?? null}, name), - url = COALESCE(${body.url ?? null}, url), - method = COALESCE(${body.method ? body.method.toUpperCase() : null}, method), - request_headers = COALESCE(${body.request_headers ? sql.json(body.request_headers) : null}, request_headers), - request_body = COALESCE(${body.request_body ?? null}, request_body), - timeout_ms = COALESCE(${body.timeout_ms ?? null}, timeout_ms), - interval_s = COALESCE(${body.interval_s ?? null}, interval_s), - max_retries = COALESCE(${body.max_retries ?? null}, max_retries), + name = COALESCE(${body.name ?? null}, name), + url = COALESCE(${body.url ?? null}, url), + method = COALESCE(${body.method ? body.method.toUpperCase() : null}, method), + request_headers = COALESCE(${body.request_headers ? sql.json(body.request_headers) : null}, request_headers), + request_body = COALESCE(${body.request_body ?? null}, request_body), + timeout_ms = COALESCE(${body.timeout_ms ?? null}, timeout_ms), + interval_s = COALESCE(${body.interval_s ?? null}, interval_s), + max_retries = COALESCE(${body.max_retries ?? null}, max_retries), retry_interval_s = COALESCE(${body.retry_interval_s ?? null}, retry_interval_s), - resend_interval = COALESCE(${body.resend_interval ?? null}, resend_interval), - cert_alert_days = COALESCE(${body.cert_alert_days ?? null}, cert_alert_days), - query = COALESCE(${body.query ? sql.json(body.query) : null}, query), - regions = COALESCE(${body.regions ? sql.array(body.regions) : null}, regions) + resend_interval = COALESCE(${body.resend_interval ?? null}, resend_interval), + cert_alert_days = COALESCE(${body.cert_alert_days ?? null}, cert_alert_days), + query = COALESCE(${body.query ? sql.json(body.query) : null}, query), + regions = COALESCE(${body.regions ? sql.array(body.regions) : null}, regions), + tags = COALESCE(${body.tags ? sql.array(dedupeTags(body.tags)) : null}, tags), + channel_ids = COALESCE(${validatedChannelIds ? sql.array(validatedChannelIds) : null}::uuid[], channel_ids), + updated_at = now() WHERE id = ${params.id} AND account_id = ${accountId} RETURNING * `; if (!monitor) { set.status = 404; return { error: "Not found" }; } - if (body.channel_ids) await replaceMonitorChannels(monitor.id, accountId, body.channel_ids); - if (body.tags) await replaceMonitorTags(monitor.id, body.tags); invalidateMonitorList(); return monitor; }, { body: t.Partial(MonitorBody), detail: { summary: "Update monitor", tags: ["monitors"] } }) diff --git a/apps/api/src/routes/pings.ts b/apps/api/src/routes/pings.ts index d87e2e1..7713100 100644 --- a/apps/api/src/routes/pings.ts +++ b/apps/api/src/routes/pings.ts @@ -81,7 +81,6 @@ export const ingest = new Elysia() if (!monitor_check) { set.status = 404; return { error: "Monitor not found" }; } const meta = body.meta ? { ...body.meta } : {}; - if (body.cert_expiry_days != null) meta.cert_expiry_days = body.cert_expiry_days; const responseBody: string | null = meta.body_preview ?? null; delete meta.body_preview; @@ -133,7 +132,7 @@ export const ingest = new Elysia() `; const [ping] = await sql` - INSERT INTO pings (monitor_id, checked_at, scheduled_at, jitter_ms, status_code, latency_ms, up, important, error, meta, region, run_id) + INSERT INTO pings (monitor_id, checked_at, scheduled_at, jitter_ms, status_code, latency_ms, up, important, error, meta, region, run_id, cert_expiry_days) VALUES ( ${body.monitor_id}, ${checkedAt ?? sql`now()`}, @@ -146,7 +145,8 @@ export const ingest = new Elysia() ${body.error ?? null}, ${Object.keys(meta).length > 0 ? sql.json(meta) : null}, ${region}, - ${body.run_id ?? null} + ${body.run_id ?? null}, + ${body.cert_expiry_days ?? null} ) RETURNING * `; diff --git a/apps/pay/src/freedom.ts b/apps/pay/src/freedom.ts index 2b8db2a..40a97db 100644 --- a/apps/pay/src/freedom.ts +++ b/apps/pay/src/freedom.ts @@ -42,6 +42,6 @@ export async function fetchQrBase64(text: string): Promise { export async function getAvailableCoins(): Promise { const info = await getChainInfo(); return Object.entries(info) - .filter(([_, v]) => v?.node?.uptime != null) + .filter(([_, v]) => v != null) .map(([k]) => k); } diff --git a/apps/shared/db.ts b/apps/shared/db.ts index c908b1a..74ac5ce 100644 --- a/apps/shared/db.ts +++ b/apps/shared/db.ts @@ -1,15 +1,20 @@ 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, - created_at TIMESTAMPTZ DEFAULT now() + 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'), @@ -23,25 +28,44 @@ export async function migrate(sql: any) { interval_s INTEGER NOT NULL DEFAULT 60, query JSONB, enabled BOOLEAN NOT NULL DEFAULT true, - created_at TIMESTAMPTZ DEFAULT now() + 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, + 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, - error TEXT, - meta JSONB + 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 ) `; + 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, @@ -49,6 +73,7 @@ export async function migrate(sql: any) { ) `; + // ── api_keys ────────────────────────────────────────────────────────── await sql` CREATE TABLE IF NOT EXISTS api_keys ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), @@ -59,24 +84,9 @@ export async function migrate(sql: any) { last_used_at TIMESTAMPTZ ) `; + await sql`CREATE INDEX IF NOT EXISTS idx_api_keys_account ON api_keys(account_id)`; - 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. + // ── monitor_region_state ────────────────────────────────────────────── await sql` CREATE TABLE IF NOT EXISTS monitor_region_state ( monitor_id TEXT NOT NULL REFERENCES monitors(id) ON DELETE CASCADE, @@ -88,11 +98,8 @@ export async function migrate(sql: any) { 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. + // ── notification_channels ───────────────────────────────────────────── await sql` CREATE TABLE IF NOT EXISTS notification_channels ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), @@ -106,28 +113,7 @@ export async function migrate(sql: any) { `; 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. + // ── status_pages ────────────────────────────────────────────────────── await sql` CREATE TABLE IF NOT EXISTS status_pages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), @@ -142,6 +128,9 @@ export async function migrate(sql: any) { show_response_time BOOLEAN NOT NULL DEFAULT true, show_cert_expiry BOOLEAN NOT NULL DEFAULT false, default_window TEXT NOT NULL DEFAULT '24h', + display_mode TEXT NOT NULL DEFAULT 'expanded', + bar_frequency TEXT NOT NULL DEFAULT 'daily', + bar_count INTEGER NOT NULL DEFAULT 90, custom_css TEXT, footer_text TEXT, og_image_url TEXT, @@ -152,14 +141,8 @@ export async function migrate(sql: any) { ) `; 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'`; - // Heartbeat bar settings: granularity + how many bars. Independent from - // default_window (which still drives the primary uptime % + multi-window cells). - await sql`ALTER TABLE status_pages ADD COLUMN IF NOT EXISTS bar_frequency TEXT NOT NULL DEFAULT 'daily'`; - await sql`ALTER TABLE status_pages ADD COLUMN IF NOT EXISTS bar_count INTEGER NOT NULL DEFAULT 90`; + // ── status_page_groups ──────────────────────────────────────────────── await sql` CREATE TABLE IF NOT EXISTS status_page_groups ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), @@ -170,20 +153,21 @@ export async function migrate(sql: any) { `; 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, + display_mode 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)`; - // Per-monitor display mode override. NULL = inherit status_pages.display_mode. - await sql`ALTER TABLE status_page_monitors ADD COLUMN IF NOT EXISTS display_mode TEXT`; + // ── incidents ───────────────────────────────────────────────────────── await sql` CREATE TABLE IF NOT EXISTS incidents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), @@ -199,6 +183,7 @@ export async function migrate(sql: any) { `; 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(), @@ -211,6 +196,7 @@ export async function migrate(sql: any) { `; 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, @@ -218,6 +204,9 @@ export async function migrate(sql: any) { 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, @@ -227,8 +216,7 @@ export async function migrate(sql: any) { `; 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. + // ── monitor_uptime_rollup ───────────────────────────────────────────── await sql` CREATE TABLE IF NOT EXISTS monitor_uptime_rollup ( monitor_id TEXT NOT NULL REFERENCES monitors(id) ON DELETE CASCADE, @@ -243,10 +231,7 @@ export async function migrate(sql: any) { `; await sql`CREATE INDEX IF NOT EXISTS idx_uptime_rollup_lookup ON monitor_uptime_rollup(monitor_id, bucket_type, bucket_start DESC)`; - // Watermark table for the rollup job. Each row records the most recent - // pings.checked_at that has been folded into a given bucket_type's rollup - // rows. Lets the periodic rollup pass scan only NEW pings since the last - // pass instead of re-aggregating the entire current bucket on every tick. + // ── rollup_watermarks ───────────────────────────────────────────────── await sql` CREATE TABLE IF NOT EXISTS rollup_watermarks ( bucket_type TEXT PRIMARY KEY, @@ -254,9 +239,7 @@ export async function migrate(sql: any) { ) `; - 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)`; - + // ── payments ────────────────────────────────────────────────────────── await sql` CREATE TABLE IF NOT EXISTS payments ( id BIGSERIAL PRIMARY KEY, @@ -269,16 +252,19 @@ export async function migrate(sql: any) { 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)`; - 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`; - + // ── payment_txs ─────────────────────────────────────────────────────── await sql` CREATE TABLE IF NOT EXISTS payment_txs ( id BIGSERIAL PRIMARY KEY, @@ -290,13 +276,8 @@ export async function migrate(sql: any) { 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"); } diff --git a/apps/web/src/routes/dashboard.ts b/apps/web/src/routes/dashboard.ts index fc8ec4c..d22f62a 100644 --- a/apps/web/src/routes/dashboard.ts +++ b/apps/web/src/routes/dashboard.ts @@ -517,10 +517,7 @@ export const dashboard = new Elysia() WHERE account_id = ${accountId} AND enabled = true ORDER BY created_at DESC `; - const attached = await sql<{ channel_id: string }[]>` - SELECT channel_id FROM monitor_notifications WHERE monitor_id = ${params.id} - `; - monitor.channel_ids = attached.map((a) => a.channel_id); + // channel_ids is already on the monitor row as a UUID[] column return html("detail", { nav: "monitors", monitor, pings, plan: resolved?.plan || "free", channels }); }) diff --git a/apps/web/src/views/detail.ejs b/apps/web/src/views/detail.ejs index 607c562..b7fd31c 100644 --- a/apps/web/src/views/detail.ejs +++ b/apps/web/src/views/detail.ejs @@ -370,7 +370,7 @@ if (ping.jitter_ms != null) html += `
Jitter
${ping.jitter_ms}ms
`; if (ping.region) html += `
Region
${escapeHtml(ping.region)}
`; if (ping.run_id) html += `
Run ID
${escapeHtml(ping.run_id)}
`; - if (meta.cert_expiry_days != null) html += `
Cert expiry
${meta.cert_expiry_days} days
`; + if (ping.cert_expiry_days != null) html += `
Cert expiry
${ping.cert_expiry_days} days
`; html += ''; // Error