pingql/apps/web/src/routes/dashboard.ts

927 lines
41 KiB
TypeScript

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 `<span class="timestamp" data-ts="${ts}">${text}</span>`;
}
const sparklineSSR = sparklineFromPings;
function latencyChartSSR(pings: any[]): string {
const data = pings.filter((c: any) => c.latency_ms != null);
if (data.length < 2) {
return '<div class="h-full flex items-center justify-center text-gray-600 text-sm">Not enough data</div>';
}
const w = 800, h = 128;
const pad = { top: 8, bottom: 8 };
const cH = h - pad.top - pad.bottom;
const runTimes: Record<string, number[]> = {};
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<string, number> = {};
runs.forEach((rid, i) => { runIndex[rid] = i; });
const maxIdx = Math.max(runs.length - 1, 1);
const byRegion: Record<string, any[]> = {};
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 += `<line x1="0" y1="${y}" x2="${w}" y2="${y}" stroke="rgba(75,85,99,0.3)" stroke-width="0.5"/>`;
}
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 += `<path d="${d}" fill="none" stroke="${color}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`;
dots += pts.filter(p => !p.up).map(p =>
`<circle cx="${p.x.toFixed(1)}" cy="${p.y.toFixed(1)}" r="3" fill="#f87171"/>`
).join('');
legend += `<span class="flex items-center gap-1"><span style="background:${color}" class="inline-block w-2 h-2 rounded-full"></span><span>${label}</span></span>`;
}
return `<div class="relative w-full h-full">
<svg viewBox="0 0 ${w} ${h}" class="w-full h-full" preserveAspectRatio="none">
${grid}${paths}${dots}
</svg>
<span class="absolute top-0 left-1 text-gray-500 text-xs leading-none pointer-events-none">${yMax}ms</span>
<span class="absolute bottom-0 left-1 text-gray-500 text-xs leading-none pointer-events-none">${yMin}ms</span>
${regions.length > 1 ? `<div class="absolute top-0 right-1 flex gap-2 text-xs text-gray-500">${legend}</div>` : ''}
</div>`;
}
function escapeHtmlSSR(str: string): string {
return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function redirect(to: string) {
return new Response(
`<!DOCTYPE html><html lang="en" class="dark"><head><meta charset="UTF-8"><meta http-equiv="refresh" content="0;url=${to}"><meta name="robots" content="noindex"><style>html,body{background:#0a0a0a;margin:0;height:100%}</style></head><body></body></html>`,
{ headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" } },
);
}
export function html(template: string, data: Record<string, unknown> = {}) {
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[<id>] - optional per-page name override
// display_mode[<id>] - '', '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<string, string> {
const out: Record<string, string> = {};
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<string, string> = {};
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<string, string> = {};
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", {}));