import { Elysia } from "elysia"; import { Eta } from "eta"; import { resolve } from "path"; import { resolveKey } from "./auth"; import sql from "../db"; import { sparklineFromPings, pickBestRegion } from "../utils/sparkline"; import { createHash } from "crypto"; import { PLAN_LABELS, REGION_COLORS, REGION_LABELS, REGIONS } from "../../../shared/plans"; const cssFile = Bun.file(resolve(import.meta.dir, "../dashboard/tailwind.css")); const cssHash = createHash("md5").update(await cssFile.bytes()).digest("hex").slice(0, 8); const jsFile = Bun.file(resolve(import.meta.dir, "../dashboard/app.js")); const jsHash = createHash("md5").update(await jsFile.bytes()).digest("hex").slice(0, 8); const qbFile = Bun.file(resolve(import.meta.dir, "../dashboard/query-builder.js")); const qbHash = createHash("md5").update(await qbFile.bytes()).digest("hex").slice(0, 8); const eta = new Eta({ views: resolve(import.meta.dir, "../views"), cache: true, defaultExtension: ".ejs" }); function timeAgoSSR(date: string | Date): string { const ts = new Date(date).getTime(); const s = Math.max(0, Math.floor((Date.now() - ts) / 1000)); const text = s < 60 ? `${s}s ago` : s < 3600 ? `${Math.floor(s/60)}m ago` : s < 86400 ? `${Math.floor(s/3600)}h ago` : `${Math.floor(s/86400)}d ago`; return `${text}`; } const sparklineSSR = sparklineFromPings; function latencyChartSSR(pings: any[]): string { const data = pings.filter((c: any) => c.latency_ms != null); if (data.length < 2) { return '
Not enough data
'; } const w = 800, h = 128; const pad = { top: 8, bottom: 8 }; const cH = h - pad.top - pad.bottom; const runTimes: Record = {}; for (const p of data) { const rid = p.run_id || p.checked_at; if (!runTimes[rid]) runTimes[rid] = []; runTimes[rid].push(new Date(p.checked_at).getTime()); } const runs = Object.keys(runTimes).sort((a, b) => { const avgA = runTimes[a].reduce((x: number, y: number) => x + y, 0) / runTimes[a].length; const avgB = runTimes[b].reduce((x: number, y: number) => x + y, 0) / runTimes[b].length; return avgA - avgB; }); const runIndex: Record = {}; runs.forEach((rid, i) => { runIndex[rid] = i; }); const maxIdx = Math.max(runs.length - 1, 1); const byRegion: Record = {}; for (const p of data) { const key = p.region || '__none__'; if (!byRegion[key]) byRegion[key] = []; byRegion[key].push(p); } const regions = Object.keys(byRegion); const allValues = data.map((c: any) => c.latency_ms); const yMax = Math.max(...allValues, 1); const yMin = Math.min(...allValues, 0); const yRange = yMax - yMin || 1; function toX(p: any): number { const idx = runIndex[p.run_id || p.checked_at] || 0; return (idx / maxIdx) * w; } function toY(v: number): number { return pad.top + cH - ((v - yMin) / yRange) * cH; } let grid = ''; for (let i = 0; i <= 4; i++) { const y = (pad.top + (cH / 4) * i).toFixed(1); grid += ``; } let paths = ''; let dots = ''; let legend = ''; for (const region of regions) { const color = REGION_COLORS[region] || '#6b7280'; const label = REGION_LABELS[region] || region; const rPings = byRegion[region].slice().sort((a: any, b: any) => toX(a) - toX(b)); const pts = rPings.map((p: any) => ({ x: toX(p), y: toY(p.latency_ms), up: p.up })); if (pts.length < 2) continue; // Catmull-Rom spline (tension 0.15, matches canvas) const t = 0.15; let d = `M${pts[0].x.toFixed(1)},${pts[0].y.toFixed(1)}`; for (let i = 0; i < pts.length - 1; i++) { const p0 = pts[Math.max(i - 1, 0)]; const p1 = pts[i]; const p2 = pts[i + 1]; const p3 = pts[Math.min(i + 2, pts.length - 1)]; const cp1x = p1.x + (p2.x - p0.x) * t; const cp1y = p1.y + (p2.y - p0.y) * t; const cp2x = p2.x - (p3.x - p1.x) * t; const cp2y = p2.y - (p3.y - p1.y) * t; d += ` C${cp1x.toFixed(1)},${cp1y.toFixed(1)} ${cp2x.toFixed(1)},${cp2y.toFixed(1)} ${p2.x.toFixed(1)},${p2.y.toFixed(1)}`; } paths += ``; dots += pts.filter(p => !p.up).map(p => `` ).join(''); legend += `${label}`; } return `
${grid}${paths}${dots} ${yMax}ms ${yMin}ms ${regions.length > 1 ? `
${legend}
` : ''}
`; } function escapeHtmlSSR(str: string): string { return str.replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } function redirect(to: string) { return new Response( ``, { headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" } }, ); } export function html(template: string, data: Record = {}) { return new Response(eta.render(template, { ...data, timeAgoSSR, sparklineSSR, pickBestRegion, latencyChartSSR, escapeHtmlSSR, cssHash, jsHash, qbHash, regionColors: REGION_COLORS, regionLabels: REGION_LABELS, regions: REGIONS, planLabels: PLAN_LABELS, }), { headers: { "content-type": "text/html; charset=utf-8" }, }); } async function getAccountId(cookie: any, headers: any): Promise<{ accountId: string; keyId: string | null; plan: string } | null> { const key = cookie?.pingql_key?.value || headers["authorization"]?.match(/^bearer\s+(.+)$/i)?.[1]?.trim(); if (!key) return null; return await resolveKey(key) ?? null; } // Parse the status page edit form's monitor list. The form posts: // monitor_order - full list of monitor IDs in DOM order (every row, not just checked) // monitor_ids - only the *checked* IDs, also in DOM order // display_name[] - optional per-page name override // display_mode[] - '', 'compact', or 'expanded' // Bun's body parser surfaces bracket-keyed fields either as nested objects // (`b.display_name = { id: value }`) or as flat string keys // (`b['display_name[id]'] = value`) depending on parser version, so handle both. function pickMap(b: any, prefix: string): Record { const out: Record = {}; if (b[prefix] && typeof b[prefix] === "object" && !Array.isArray(b[prefix])) { for (const [k, v] of Object.entries(b[prefix])) { if (typeof v === "string" && v.trim()) out[k] = v.trim(); } } else { const re = new RegExp(`^${prefix}\\[(.+)\\]$`); for (const k of Object.keys(b)) { const m = k.match(re); if (m && typeof b[k] === "string" && b[k].trim()) out[m[1]!] = b[k].trim(); } } return out; } function parseStatusPageMonitors(b: any): { monitorIds: string[]; monitorsForApi: Array<{ monitor_id: string; position: number; display_name: string | null; display_mode: string | null }>; } { const order: string[] = Array.isArray(b.monitor_order) ? b.monitor_order : (b.monitor_order ? [b.monitor_order] : []); const checked: string[] = Array.isArray(b.monitor_ids) ? b.monitor_ids : (b.monitor_ids ? [b.monitor_ids] : []); const checkedSet = new Set(checked); const displayNames = pickMap(b, "display_name"); const displayModes = pickMap(b, "display_mode"); // Walk the rendered order and keep only the checked monitors. Position is // their index in this filtered list. const monitorIds: string[] = []; const monitorsForApi: Array<{ monitor_id: string; position: number; display_name: string | null; display_mode: string | null }> = []; for (const id of order) { if (!checkedSet.has(id)) continue; monitorsForApi.push({ monitor_id: id, position: monitorIds.length, display_name: displayNames[id] ?? null, display_mode: (displayModes[id] === "compact" || displayModes[id] === "expanded") ? displayModes[id]! : null, }); monitorIds.push(id); } // If the form somehow posted a checked ID that wasn't in the order list // (shouldn't happen, defensive), append it at the end. for (const id of checked) { if (monitorIds.includes(id)) continue; monitorsForApi.push({ monitor_id: id, position: monitorIds.length, display_name: displayNames[id] ?? null, display_mode: (displayModes[id] === "compact" || displayModes[id] === "expanded") ? displayModes[id]! : null, }); monitorIds.push(id); } return { monitorIds, monitorsForApi }; } const dashDir = resolve(import.meta.dir, "../dashboard"); export const dashboard = new Elysia() .get("/", () => html("landing", {})) .get("/favicon.svg", () => new Response(Bun.file(`${dashDir}/favicon.svg`), { headers: { "content-type": "image/svg+xml", "cache-control": "public, max-age=86400" } })) .get("/assets/tailwind.css", () => new Response(Bun.file(`${dashDir}/tailwind.css`), { headers: { "cache-control": "public, max-age=31536000, immutable" } })) .get("/assets/app.css", () => new Response(Bun.file(`${dashDir}/app.css`), { headers: { "cache-control": "public, max-age=31536000, immutable" } })) .get("/assets/app.js", () => new Response(Bun.file(`${dashDir}/app.js`), { headers: { "cache-control": "public, max-age=31536000, immutable" } })) .get("/dashboard/query-builder.js", () => new Response(Bun.file(`${dashDir}/query-builder.js`), { headers: { "cache-control": "public, max-age=31536000, immutable" } })) .get("/dashboard", async ({ cookie }) => { const key = cookie?.pingql_key?.value; if (key) { const resolved = await resolveKey(key); if (resolved) return redirect("/dashboard/home"); // Invalid/stale key - clear it and show login cookie.pingql_key?.remove(); } return html("login", {}); }) .get("/dashboard/logout", ({ cookie }) => { cookie.pingql_key?.set({ value: "", maxAge: 0, path: "/", domain: process.env.COOKIE_DOMAIN ?? ".pingql.com", secure: process.env.NODE_ENV !== "development", sameSite: "lax" }); return redirect("/dashboard"); }) .get("/dashboard/welcome", async ({ cookie, headers, query }) => { const resolved = await getAccountId(cookie, headers); if (!resolved?.accountId) return redirect("/dashboard"); return html("welcome", { key: query.key || cookie?.pingql_key?.value || "" }); }) .get("/dashboard/home", async ({ cookie, headers }) => { const resolved = await getAccountId(cookie, headers); const accountId = resolved?.accountId ?? null; const keyId = resolved?.keyId ?? null; if (!accountId) return redirect("/dashboard"); const monitors = await sql` SELECT * FROM monitors WHERE account_id = ${accountId} ORDER BY created_at DESC `; // One simple indexed query per monitor, run in parallel. Each is an index // seek on (monitor_id, checked_at DESC) - microseconds. Trivially scalable // and easy to reason about. const pingResults = await Promise.all( monitors.map((m: any) => sql` SELECT id, checked_at, latency_ms, up, region FROM pings WHERE monitor_id = ${m.id} ORDER BY checked_at DESC LIMIT 60 `) ); const monitorsWithPings = monitors.map((m: any, i: number) => { const recent = pingResults[i] as any[]; // DESC order return { ...m, last_ping: recent[0] ?? null, pings: recent.slice().reverse(), // sparkline expects chronological ASC }; }); return html("home", { nav: "monitors", monitors: monitorsWithPings, accountId }); }) .get("/dashboard/settings", async ({ cookie, headers }) => { const resolved = await getAccountId(cookie, headers); const accountId = resolved?.accountId ?? null; const keyId = resolved?.keyId ?? null; if (!accountId) return redirect("/dashboard"); const isSubKey = !!keyId; const loginKey = isSubKey ? null : (cookie?.pingql_key?.value ?? null); // All four reads are independent - fan them out in parallel instead of // serializing four round-trips. Each individual query is fast (PK seek or // small indexed scan); the win is just halving the wall-clock by not // waiting on each one in turn. const accountQ = sql`SELECT id, email_hash, plan, plan_expires_at, plan_stack, created_at FROM accounts WHERE id = ${accountId}`; const apiKeysQ = isSubKey ? Promise.resolve([] as any[]) : sql`SELECT id, key, label, created_at, last_used_at FROM api_keys WHERE account_id = ${accountId} ORDER BY created_at DESC`; const monitorCountQ = sql`SELECT COUNT(*)::int as count FROM monitors WHERE account_id = ${accountId}`; const invoicesQ = sql` SELECT id, plan, months, amount_usd, coin, amount_crypto, status, created_at, paid_at, expires_at, txid FROM payments WHERE account_id = ${accountId} AND (status = 'paid' OR (status IN ('pending', 'underpaid', 'confirming') AND expires_at >= now())) ORDER BY created_at DESC LIMIT 20 `.catch(() => [] as any[]); const [accountRows, apiKeys, monitorCountRows, invoices] = await Promise.all([ accountQ, apiKeysQ, monitorCountQ, invoicesQ, ]); const acc = accountRows[0]; const monitorCount = monitorCountRows[0].count; return html("settings", { nav: "settings", account: acc, apiKeys, accountId, loginKey, isSubKey, monitorCount, invoices }); }) .get("/dashboard/checkout", async ({ cookie, headers }) => { const resolved = await getAccountId(cookie, headers); if (!resolved?.accountId) return redirect("/dashboard"); const [acc] = await sql`SELECT plan, plan_expires_at, plan_stack FROM accounts WHERE id = ${resolved.accountId}`; const stack = typeof acc.plan_stack === "string" ? JSON.parse(acc.plan_stack) : (acc.plan_stack || []); const hasLifetime = acc.plan === "lifetime" || stack.some((s: any) => s.plan === "lifetime"); if (acc.plan === "lifetime" && stack.length === 0) return redirect("/dashboard/settings"); const [{ total_spent }] = await sql`SELECT COALESCE(SUM(amount_usd), 0)::numeric as total_spent FROM payments WHERE account_id = ${resolved.accountId} AND status = 'paid'`; const payApi = process.env.PAY_API || "https://pay.pingql.com"; let coins: any[] = []; try { const res = await fetch(`${payApi}/coins`); const data = await res.json(); coins = data.coins || []; } catch {} return html("checkout", { nav: "settings", account: acc, payApi, invoiceId: null, coins, invoice: null, totalSpent: Number(total_spent), hasLifetime }); }) .get("/dashboard/checkout/:id", async ({ cookie, headers, params }) => { const resolved = await getAccountId(cookie, headers); if (!resolved?.accountId) return redirect("/dashboard"); const [acc] = await sql`SELECT plan, plan_expires_at FROM accounts WHERE id = ${resolved.accountId}`; const payApi = process.env.PAY_API || "https://pay.pingql.com"; const key = cookie?.pingql_key?.value; let invoice: any = null; let coins: any[] = []; try { const [invoiceRes, coinsRes] = await Promise.all([ fetch(`${payApi}/checkout/${params.id}`, { headers: { "Authorization": `Bearer ${key}` } }), fetch(`${payApi}/coins`), ]); if (invoiceRes.ok) invoice = await invoiceRes.json(); const coinsData = await coinsRes.json(); coins = coinsData.coins || []; } catch {} return html("checkout", { nav: "settings", account: acc, payApi, invoiceId: params.id, coins, invoice }); }) .get("/dashboard/checkout/:id/receipt", async ({ cookie, headers, params, set }) => { const resolved = await getAccountId(cookie, headers); if (!resolved?.accountId) return redirect("/dashboard"); const payApi = process.env.PAY_API || "https://pay.pingql.com"; const key = cookie?.pingql_key?.value; try { const res = await fetch(`${payApi}/checkout/${params.id}/receipt`, { headers: { "Authorization": `Bearer ${key}` }, }); if (!res.ok) { const data = await res.json().catch(() => ({})); set.status = res.status; return data.error || "Receipt not available"; } set.headers["content-type"] = "text/html; charset=utf-8"; return await res.text(); } catch { set.status = 500; return "Could not load receipt"; } }) .post("/dashboard/checkout", async ({ cookie, headers, body }) => { const resolved = await getAccountId(cookie, headers); if (!resolved?.accountId) return redirect("/dashboard"); const payApi = process.env.PAY_API || "https://pay.pingql.com"; const key = cookie?.pingql_key?.value; const b = body as any; try { const res = await fetch(`${payApi}/checkout`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` }, body: JSON.stringify({ plan: b.plan, coin: b.coin, months: b.months ? Number(b.months) : undefined, }), }); const data = await res.json(); if (res.ok && data.id) return redirect(`/dashboard/checkout/${data.id}`); } catch {} return redirect("/dashboard/checkout"); }) .get("/dashboard/monitors/new", async ({ cookie, headers }) => { const resolved = await getAccountId(cookie, headers); const accountId = resolved?.accountId ?? null; if (!accountId) return redirect("/dashboard"); const channels = await sql` SELECT id, name, kind FROM notification_channels WHERE account_id = ${accountId} AND enabled = true ORDER BY created_at DESC `; return html("new", { nav: "monitors", plan: resolved?.plan || "free", channels }); }) .get("/dashboard/notifications", async ({ cookie, headers, query }) => { const resolved = await getAccountId(cookie, headers); if (!resolved?.accountId) return redirect("/dashboard"); const channels = await sql` SELECT id, name, kind, config, enabled, created_at FROM notification_channels WHERE account_id = ${resolved.accountId} ORDER BY created_at DESC `; const testResult = query.test === "ok" ? { ok: true } : query.test_error ? { ok: false, error: String(query.test_error) } : null; return html("notifications", { nav: "notifications", channels, testResult }); }) .post("/dashboard/notifications/new", async ({ cookie, headers, body }) => { const resolved = await getAccountId(cookie, headers); if (!resolved?.accountId) return redirect("/dashboard"); const b = body as any; const kind = b.kind || "webhook"; const config: any = {}; if (kind === "webhook") { config.url = (b.url || "").trim(); if (b.secret) config.secret = b.secret; } try { const apiUrl = process.env.API_URL || "https://api.pingql.com"; const key = cookie?.pingql_key?.value; await fetch(`${apiUrl}/notifications/channels/`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` }, body: JSON.stringify({ name: (b.name || "").trim(), kind, config }), }); } catch {} return redirect("/dashboard/notifications"); }) .post("/dashboard/notifications/:id/delete", async ({ cookie, headers, params }) => { const resolved = await getAccountId(cookie, headers); if (!resolved?.accountId) return redirect("/dashboard"); try { const apiUrl = process.env.API_URL || "https://api.pingql.com"; const key = cookie?.pingql_key?.value; await fetch(`${apiUrl}/notifications/channels/${params.id}`, { method: "DELETE", headers: { "Authorization": `Bearer ${key}` }, }); } catch {} return redirect("/dashboard/notifications"); }) .post("/dashboard/notifications/:id/test", async ({ cookie, headers, params }) => { const resolved = await getAccountId(cookie, headers); if (!resolved?.accountId) return redirect("/dashboard"); try { const apiUrl = process.env.API_URL || "https://api.pingql.com"; const key = cookie?.pingql_key?.value; const res = await fetch(`${apiUrl}/notifications/channels/${params.id}/test`, { method: "POST", headers: { "Authorization": `Bearer ${key}` }, }); if (res.ok) return redirect("/dashboard/notifications?test=ok"); const data: any = await res.json().catch(() => ({})); return redirect(`/dashboard/notifications?test_error=${encodeURIComponent(data.error || res.statusText)}`); } catch (e: any) { return redirect(`/dashboard/notifications?test_error=${encodeURIComponent(e?.message || "request failed")}`); } }) .get("/dashboard/home/data", async ({ cookie, headers }) => { const resolved = await getAccountId(cookie, headers); const accountId = resolved?.accountId ?? null; const keyId = resolved?.keyId ?? null; if (!accountId) return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 }); const monitors = await sql` SELECT id FROM monitors WHERE account_id = ${accountId} ORDER BY created_at DESC `; return new Response(JSON.stringify({ monitorIds: monitors.map((m: any) => m.id) }), { headers: { "content-type": "application/json" }, }); }) .get("/dashboard/monitors/:id", async ({ cookie, headers, params }) => { const resolved = await getAccountId(cookie, headers); const accountId = resolved?.accountId ?? null; const keyId = resolved?.keyId ?? null; if (!accountId) return redirect("/dashboard"); const [monitor] = await sql` SELECT * FROM monitors WHERE id = ${params.id} AND account_id = ${accountId} `; if (!monitor) return redirect("/dashboard/home"); const pings = await sql` SELECT * FROM pings WHERE monitor_id = ${params.id} ORDER BY checked_at DESC LIMIT 200 `; const channels = await sql` SELECT id, name, kind FROM notification_channels WHERE account_id = ${accountId} AND enabled = true ORDER BY created_at DESC `; // channel_ids is already on the monitor row as a UUID[] column return html("detail", { nav: "monitors", monitor, pings, plan: resolved?.plan || "free", channels }); }) .get("/dashboard/monitors/:id/chart", async ({ cookie, headers, params }) => { const resolved = await getAccountId(cookie, headers); const accountId = resolved?.accountId ?? null; const keyId = resolved?.keyId ?? null; if (!accountId) return new Response("Unauthorized", { status: 401 }); const [monitor] = await sql` SELECT id FROM monitors WHERE id = ${params.id} AND account_id = ${accountId} `; if (!monitor) return new Response("Not found", { status: 404 }); const pings = await sql` SELECT * FROM pings WHERE monitor_id = ${params.id} ORDER BY checked_at DESC LIMIT 200 `; const chartPings = pings.slice().reverse(); return new Response(latencyChartSSR(chartPings), { headers: { "content-type": "text/html; charset=utf-8" }, }); }) .get("/dashboard/monitors/:id/sparkline", async ({ cookie, headers, params }) => { const resolved = await getAccountId(cookie, headers); const accountId = resolved?.accountId ?? null; const keyId = resolved?.keyId ?? null; if (!accountId) return new Response("Unauthorized", { status: 401 }); const [monitor] = await sql` SELECT id FROM monitors WHERE id = ${params.id} AND account_id = ${accountId} `; if (!monitor) return new Response("Not found", { status: 404 }); const pings = await sql` SELECT latency_ms, region FROM pings WHERE monitor_id = ${params.id} AND latency_ms IS NOT NULL ORDER BY checked_at DESC LIMIT 40 `; return new Response(sparklineSSR(pings.slice().reverse()), { headers: { "content-type": "text/html; charset=utf-8" }, }); }) .post("/dashboard/monitors/new", async ({ cookie, headers, body, set }) => { const resolved = await getAccountId(cookie, headers); if (!resolved?.accountId) return redirect("/dashboard"); const b = body as any; const regions = Array.isArray(b.regions) ? b.regions : (b.regions ? [b.regions] : []); const query = b.query ? (typeof b.query === "string" ? JSON.parse(b.query) : b.query) : undefined; const requestHeaders: Record = {}; const hKeys = Array.isArray(b.header_key) ? b.header_key : (b.header_key ? [b.header_key] : []); const hVals = Array.isArray(b.header_value) ? b.header_value : (b.header_value ? [b.header_value] : []); for (let i = 0; i < hKeys.length; i++) { if (hKeys[i]?.trim()) requestHeaders[hKeys[i].trim()] = hVals[i] || ""; } try { const apiUrl = process.env.API_URL || "https://api.pingql.com"; const key = cookie?.pingql_key?.value; await fetch(`${apiUrl}/monitors/`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` }, body: JSON.stringify({ name: b.name, url: b.url, method: b.method || "GET", interval_s: Number(b.interval_s) || 30, timeout_ms: Number(b.timeout_ms) || 10000, max_retries: Number(b.max_retries) || 0, retry_interval_s: Number(b.retry_interval_s) || undefined, resend_interval: Number(b.resend_interval) || 0, cert_alert_days: b.cert_alert_days != null ? Number(b.cert_alert_days) : 0, channel_ids: Array.isArray(b.channel_ids) ? b.channel_ids : (b.channel_ids ? [b.channel_ids] : []), regions, request_headers: Object.keys(requestHeaders).length ? requestHeaders : null, request_body: b.request_body || null, query, }), }); } catch {} return redirect("/dashboard/home"); }) .post("/dashboard/monitors/:id/edit", async ({ cookie, headers, params, body }) => { const resolved = await getAccountId(cookie, headers); if (!resolved?.accountId) return redirect("/dashboard"); const b = body as any; const regions = Array.isArray(b.regions) ? b.regions : (b.regions ? [b.regions] : []); const query = b.query ? (typeof b.query === "string" ? JSON.parse(b.query) : b.query) : undefined; const requestHeaders: Record = {}; const hKeys = Array.isArray(b.header_key) ? b.header_key : (b.header_key ? [b.header_key] : []); const hVals = Array.isArray(b.header_value) ? b.header_value : (b.header_value ? [b.header_value] : []); for (let i = 0; i < hKeys.length; i++) { if (hKeys[i]?.trim()) requestHeaders[hKeys[i].trim()] = hVals[i] || ""; } try { const apiUrl = process.env.API_URL || "https://api.pingql.com"; const key = cookie?.pingql_key?.value; await fetch(`${apiUrl}/monitors/${params.id}`, { method: "PATCH", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` }, body: JSON.stringify({ name: b.name, url: b.url, method: b.method || "GET", interval_s: Number(b.interval_s) || 30, timeout_ms: Number(b.timeout_ms) || 10000, max_retries: Number(b.max_retries) || 0, retry_interval_s: Number(b.retry_interval_s) || undefined, resend_interval: Number(b.resend_interval) || 0, cert_alert_days: b.cert_alert_days != null ? Number(b.cert_alert_days) : 0, channel_ids: Array.isArray(b.channel_ids) ? b.channel_ids : (b.channel_ids ? [b.channel_ids] : []), regions, request_headers: Object.keys(requestHeaders).length ? requestHeaders : null, request_body: b.request_body || null, query, }), }); } catch {} return redirect(`/dashboard/monitors/${params.id}`); }) .post("/dashboard/monitors/:id/delete", async ({ cookie, headers, params }) => { const resolved = await getAccountId(cookie, headers); if (!resolved?.accountId) return redirect("/dashboard"); const apiUrl = process.env.API_URL || "https://api.pingql.com"; const key = cookie?.pingql_key?.value; await fetch(`${apiUrl}/monitors/${params.id}`, { method: "DELETE", headers: { "Authorization": `Bearer ${key}` }, }); return redirect("/dashboard/home"); }) .post("/dashboard/monitors/:id/toggle", async ({ cookie, headers, params }) => { const resolved = await getAccountId(cookie, headers); if (!resolved?.accountId) return redirect("/dashboard"); const apiUrl = process.env.API_URL || "https://api.pingql.com"; const key = cookie?.pingql_key?.value; await fetch(`${apiUrl}/monitors/${params.id}/toggle`, { method: "POST", headers: { "Authorization": `Bearer ${key}` }, }); return redirect(`/dashboard/monitors/${params.id}`); }) // ── Status pages ────────────────────────────────────────────────── .get("/dashboard/status-pages", async ({ cookie, headers }) => { const resolved = await getAccountId(cookie, headers); if (!resolved?.accountId) return redirect("/dashboard"); const pages = await sql` SELECT id, slug, title, description, theme, default_window FROM status_pages WHERE account_id = ${resolved.accountId} ORDER BY created_at DESC `; return html("status-pages", { nav: "status-pages", pages }); }) .get("/dashboard/status-pages/new", async ({ cookie, headers }) => { const resolved = await getAccountId(cookie, headers); if (!resolved?.accountId) return redirect("/dashboard"); const allMonitors = await sql` SELECT id, name FROM monitors WHERE account_id = ${resolved.accountId} ORDER BY created_at DESC `; return html("status-page-edit", { nav: "status-pages", isNew: true, page: null, allMonitors }); }) .get("/dashboard/status-pages/:id", async ({ cookie, headers, params }) => { const resolved = await getAccountId(cookie, headers); if (!resolved?.accountId) return redirect("/dashboard"); const [page] = await sql` SELECT * FROM status_pages WHERE id = ${params.id} AND account_id = ${resolved.accountId} `; if (!page) return redirect("/dashboard/status-pages"); const monitors = await sql` SELECT monitor_id, display_name, display_mode FROM status_page_monitors WHERE status_page_id = ${params.id} ORDER BY position ASC `; const allMonitors = await sql` SELECT id, name FROM monitors WHERE account_id = ${resolved.accountId} ORDER BY created_at DESC `; page.monitors = monitors; return html("status-page-edit", { nav: "status-pages", isNew: false, page, allMonitors }); }) .post("/dashboard/status-pages/new", async ({ cookie, headers, body }) => { const resolved = await getAccountId(cookie, headers); if (!resolved?.accountId) return redirect("/dashboard"); const b = body as any; const { monitorsForApi } = parseStatusPageMonitors(b); try { const apiUrl = process.env.API_URL || "https://api.pingql.com"; const key = cookie?.pingql_key?.value; await fetch(`${apiUrl}/pages/`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` }, body: JSON.stringify({ slug: (b.slug || "").trim(), title: b.title, description: b.description || null, theme: b.theme || "auto", default_window: b.default_window || "24h", display_mode: b.display_mode || "expanded", bar_frequency: b.bar_frequency || "daily", bar_count: Number(b.bar_count) || 90, show_response_time: !!b.show_response_time, show_powered_by: !!b.show_powered_by, index_search: !!b.index_search, password: b.password || undefined, custom_css: b.custom_css || null, footer_text: b.footer_text || null, monitors: monitorsForApi, }), }); } catch {} return redirect("/dashboard/status-pages"); }) .post("/dashboard/status-pages/:id/edit", async ({ cookie, headers, params, body }) => { const resolved = await getAccountId(cookie, headers); if (!resolved?.accountId) return redirect("/dashboard"); const b = body as any; const { monitorsForApi } = parseStatusPageMonitors(b); try { const apiUrl = process.env.API_URL || "https://api.pingql.com"; const key = cookie?.pingql_key?.value; const payload: any = { slug: (b.slug || "").trim(), title: b.title, description: b.description || null, theme: b.theme || "auto", default_window: b.default_window || "24h", display_mode: b.display_mode || "expanded", bar_frequency: b.bar_frequency || "daily", bar_count: Number(b.bar_count) || 90, show_response_time: !!b.show_response_time, show_powered_by: !!b.show_powered_by, index_search: !!b.index_search, custom_css: b.custom_css || null, footer_text: b.footer_text || null, monitors: monitorsForApi, }; // Three cases for the password field: // 1. user clicked Remove → hidden `remove_password=1` is set, send null // (the API treats `password === null` as "clear the password_hash") // 2. user typed a new password → send the string (API hashes + replaces) // 3. neither → omit the field entirely so the API leaves password_hash // alone. Empty string falls through to this branch on purpose. if (b.remove_password) payload.password = null; else if (b.password) payload.password = b.password; await fetch(`${apiUrl}/pages/${params.id}`, { method: "PATCH", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` }, body: JSON.stringify(payload), }); } catch {} return redirect("/dashboard/status-pages"); }) .post("/dashboard/status-pages/:id/delete", async ({ cookie, headers, params }) => { const resolved = await getAccountId(cookie, headers); if (!resolved?.accountId) return redirect("/dashboard"); try { const apiUrl = process.env.API_URL || "https://api.pingql.com"; const key = cookie?.pingql_key?.value; await fetch(`${apiUrl}/pages/${params.id}`, { method: "DELETE", headers: { "Authorization": `Bearer ${key}` }, }); } catch {} return redirect("/dashboard/status-pages"); }) // ── Incidents ───────────────────────────────────────────────────── .get("/dashboard/incidents", async ({ cookie, headers }) => { const resolved = await getAccountId(cookie, headers); if (!resolved?.accountId) return redirect("/dashboard"); const incidents = await sql` SELECT id, title, status, severity, pinned, started_at, resolved_at FROM incidents WHERE account_id = ${resolved.accountId} ORDER BY started_at DESC LIMIT 200 `; return html("incidents", { nav: "incidents", incidents }); }) .get("/dashboard/incidents/new", async ({ cookie, headers }) => { const resolved = await getAccountId(cookie, headers); if (!resolved?.accountId) return redirect("/dashboard"); const allMonitors = await sql`SELECT id, name FROM monitors WHERE account_id = ${resolved.accountId} ORDER BY created_at DESC`; const allPages = await sql`SELECT id, title FROM status_pages WHERE account_id = ${resolved.accountId} ORDER BY created_at DESC`; return html("incident-edit", { nav: "incidents", isNew: true, incident: null, allMonitors, allPages }); }) .get("/dashboard/incidents/:id", async ({ cookie, headers, params }) => { const resolved = await getAccountId(cookie, headers); if (!resolved?.accountId) return redirect("/dashboard"); const [incident] = await sql` SELECT * FROM incidents WHERE id = ${params.id} AND account_id = ${resolved.accountId} `; if (!incident) return redirect("/dashboard/incidents"); const updates = await sql`SELECT * FROM incident_updates WHERE incident_id = ${params.id} ORDER BY created_at ASC`; const monitors = await sql`SELECT monitor_id FROM incident_monitors WHERE incident_id = ${params.id}`; const pages = await sql`SELECT status_page_id FROM incident_status_pages WHERE incident_id = ${params.id}`; incident.updates = updates; incident.monitor_ids = monitors.map((m: any) => m.monitor_id); incident.status_page_ids = pages.map((p: any) => p.status_page_id); const allMonitors = await sql`SELECT id, name FROM monitors WHERE account_id = ${resolved.accountId} ORDER BY created_at DESC`; const allPages = await sql`SELECT id, title FROM status_pages WHERE account_id = ${resolved.accountId} ORDER BY created_at DESC`; return html("incident-edit", { nav: "incidents", isNew: false, incident, allMonitors, allPages }); }) .post("/dashboard/incidents/new", async ({ cookie, headers, body }) => { const resolved = await getAccountId(cookie, headers); if (!resolved?.accountId) return redirect("/dashboard"); const b = body as any; const monitorIds = Array.isArray(b.monitor_ids) ? b.monitor_ids : (b.monitor_ids ? [b.monitor_ids] : []); const pageIds = Array.isArray(b.status_page_ids) ? b.status_page_ids : (b.status_page_ids ? [b.status_page_ids] : []); try { const apiUrl = process.env.API_URL || "https://api.pingql.com"; const key = cookie?.pingql_key?.value; await fetch(`${apiUrl}/incidents/`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` }, body: JSON.stringify({ title: b.title, status: b.status || "investigating", severity: b.severity || "minor", monitor_ids: monitorIds, status_page_ids: pageIds, initial_update: { body: b.initial_update_body || "Investigating." }, }), }); } catch {} return redirect("/dashboard/incidents"); }) .post("/dashboard/incidents/:id/edit", async ({ cookie, headers, params, body }) => { const resolved = await getAccountId(cookie, headers); if (!resolved?.accountId) return redirect("/dashboard"); const b = body as any; const monitorIds = Array.isArray(b.monitor_ids) ? b.monitor_ids : (b.monitor_ids ? [b.monitor_ids] : []); const pageIds = Array.isArray(b.status_page_ids) ? b.status_page_ids : (b.status_page_ids ? [b.status_page_ids] : []); try { const apiUrl = process.env.API_URL || "https://api.pingql.com"; const key = cookie?.pingql_key?.value; await fetch(`${apiUrl}/incidents/${params.id}`, { method: "PATCH", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` }, body: JSON.stringify({ title: b.title, status: b.status, severity: b.severity, monitor_ids: monitorIds, status_page_ids: pageIds, }), }); } catch {} return redirect(`/dashboard/incidents/${params.id}`); }) .post("/dashboard/incidents/:id/update", async ({ cookie, headers, params, body }) => { const resolved = await getAccountId(cookie, headers); if (!resolved?.accountId) return redirect("/dashboard"); const b = body as any; try { const apiUrl = process.env.API_URL || "https://api.pingql.com"; const key = cookie?.pingql_key?.value; await fetch(`${apiUrl}/incidents/${params.id}/updates`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` }, body: JSON.stringify({ status: b.status, body: b.body }), }); } catch {} return redirect(`/dashboard/incidents/${params.id}`); }) .post("/dashboard/incidents/:id/delete", async ({ cookie, headers, params }) => { const resolved = await getAccountId(cookie, headers); if (!resolved?.accountId) return redirect("/dashboard"); try { const apiUrl = process.env.API_URL || "https://api.pingql.com"; const key = cookie?.pingql_key?.value; await fetch(`${apiUrl}/incidents/${params.id}`, { method: "DELETE", headers: { "Authorization": `Bearer ${key}` }, }); } catch {} return redirect("/dashboard/incidents"); }) .get("/docs", () => html("docs", {})) .get("/privacy", () => html("privacy", {})) .get("/terms", () => html("tos", {}));