refactor db
This commit is contained in:
parent
a58030323a
commit
29a9d6cea6
|
|
@ -32,8 +32,8 @@ export async function dispatchForMonitor(monitorId: string, event: NotificationE
|
|||
const channels = await sql<ChannelRow[]>`
|
||||
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)));
|
||||
|
|
|
|||
|
|
@ -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"] } })
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string[]> {
|
||||
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,6 +143,7 @@ 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),
|
||||
|
|
@ -172,13 +158,14 @@ export const monitors = new Elysia({ prefix: "/monitors" })
|
|||
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)
|
||||
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"] } })
|
||||
|
|
|
|||
|
|
@ -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 *
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -42,6 +42,6 @@ export async function fetchQrBase64(text: string): Promise<string> {
|
|||
export async function getAvailableCoins(): Promise<string[]> {
|
||||
const info = await getChainInfo();
|
||||
return Object.entries(info)
|
||||
.filter(([_, v]) => v?.node?.uptime != null)
|
||||
.filter(([_, v]) => v != null)
|
||||
.map(([k]) => k);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
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,10 +28,22 @@ 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,
|
||||
|
|
@ -37,11 +54,18 @@ export async function migrate(sql: any) {
|
|||
status_code INTEGER,
|
||||
latency_ms INTEGER,
|
||||
up BOOLEAN NOT NULL,
|
||||
important BOOLEAN NOT NULL DEFAULT false,
|
||||
error TEXT,
|
||||
meta JSONB
|
||||
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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
})
|
||||
|
|
|
|||
|
|
@ -370,7 +370,7 @@
|
|||
if (ping.jitter_ms != null) html += `<div class="text-gray-500">Jitter</div><div class="text-gray-300">${ping.jitter_ms}ms</div>`;
|
||||
if (ping.region) html += `<div class="text-gray-500">Region</div><div class="text-gray-300">${escapeHtml(ping.region)}</div>`;
|
||||
if (ping.run_id) html += `<div class="text-gray-500">Run ID</div><div class="text-gray-300 font-mono break-all">${escapeHtml(ping.run_id)}</div>`;
|
||||
if (meta.cert_expiry_days != null) html += `<div class="text-gray-500">Cert expiry</div><div class="text-gray-300">${meta.cert_expiry_days} days</div>`;
|
||||
if (ping.cert_expiry_days != null) html += `<div class="text-gray-500">Cert expiry</div><div class="text-gray-300">${ping.cert_expiry_days} days</div>`;
|
||||
html += '</div>';
|
||||
|
||||
// Error
|
||||
|
|
|
|||
Loading…
Reference in New Issue