import { Elysia } from "elysia"; import { Eta } from "eta"; import { resolve } from "path"; import { resolveKey } from "./auth"; import sql from "../db"; import { sparkline } from "../utils/sparkline"; 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.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 = sparkline; function latencyChartSSR(pings: any[]): string { const data = pings.filter((c: any) => c.latency_ms != null); if (data.length < 2) { return '
Not enough data
'; } const values = data.map((c: any) => c.latency_ms); const ups = data.map((c: any) => c.up); const max = Math.max(...values, 1); const min = Math.min(...values, 0); const range = max - min || 1; const w = 800; const h = 128; const step = w / Math.max(values.length - 1, 1); const points = values.map((v: number, i: number) => { const x = i * step; const y = h - ((v - min) / range) * (h - 16) - 8; return [x, y]; }); const pathD = points.map((p: number[], i: number) => `${i === 0 ? 'M' : 'L'}${p[0]},${p[1]}`).join(' '); const areaD = pathD + ` L${points[points.length - 1][0]},${h} L${points[0][0]},${h} Z`; const dots = points.map((p: number[], i: number) => !ups[i] ? `` : '' ).join(''); return `
${dots} ${max}ms ${min}ms
`; } function escapeHtmlSSR(str: string): string { return str.replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } function html(template: string, data: Record = {}) { return new Response(eta.render(template, { ...data, timeAgoSSR, sparklineSSR, latencyChartSSR, escapeHtmlSSR }), { headers: { "content-type": "text/html; charset=utf-8" }, }); } function redirect(to: string) { return new Response(null, { status: 302, headers: { Location: to } }); } async function getAccountId(cookie: any, headers: any): Promise { const authHeader = headers["authorization"] ?? ""; const bearer = authHeader.match(/^bearer\s+(.+)$/i)?.[1]?.trim(); const key = cookie?.pingql_key?.value || bearer; if (!key) return null; const resolved = await resolveKey(key); return resolved?.accountId ?? null; } const dashDir = resolve(import.meta.dir, "../dashboard"); export const dashboard = new Elysia() .get("/dashboard/app.js", () => Bun.file(`${dashDir}/app.js`)) .get("/dashboard/app.css", () => Bun.file(`${dashDir}/app.css`)) .get("/dashboard/query-builder.js", () => Bun.file(`${dashDir}/query-builder.js`)) // Login page .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 Bun.file(`${dashDir}/index.html`); }) // Logout .get("/dashboard/logout", ({ cookie }) => { cookie.pingql_key?.remove(); return redirect("/dashboard"); }) // Home — SSR monitor list .get("/dashboard/home", async ({ cookie, headers }) => { const accountId = await getAccountId(cookie, headers); if (!accountId) return redirect("/dashboard"); const monitors = await sql` SELECT m.*, ( SELECT row_to_json(p) FROM pings p WHERE p.monitor_id = m.id ORDER BY p.checked_at DESC LIMIT 1 ) as last_ping FROM monitors m WHERE m.account_id = ${accountId} ORDER BY m.created_at DESC `; // Fetch last 20 pings per monitor for sparklines const monitorIds = monitors.map((m: any) => m.id); let pingsMap: Record = {}; if (monitorIds.length > 0) { const allPings = await sql` SELECT * FROM ( SELECT p.*, ROW_NUMBER() OVER (PARTITION BY p.monitor_id ORDER BY p.checked_at DESC) as rn FROM pings p WHERE p.monitor_id = ANY(${monitorIds}) ) sub WHERE rn <= 20 ORDER BY monitor_id, checked_at ASC `; for (const p of allPings) { if (!pingsMap[p.monitor_id]) pingsMap[p.monitor_id] = []; pingsMap[p.monitor_id].push(p); } } const monitorsWithPings = monitors.map((m: any) => ({ ...m, pings: pingsMap[m.id] || [], })); return html("home", { nav: "monitors", monitors: monitorsWithPings, accountId }); }) // Settings — SSR account info .get("/dashboard/settings", async ({ cookie, headers }) => { const accountId = await getAccountId(cookie, headers); if (!accountId) return redirect("/dashboard"); const [acc] = await sql`SELECT id, email_hash, created_at FROM accounts WHERE id = ${accountId}`; const apiKeys = await sql`SELECT id, key, label, created_at, last_used_at FROM api_keys WHERE account_id = ${accountId} ORDER BY created_at DESC`; const loginKey = cookie?.pingql_key?.value ?? null; return html("settings", { nav: "settings", account: acc, apiKeys, accountId, loginKey }); }) // New monitor .get("/dashboard/monitors/new", async ({ cookie, headers }) => { const accountId = await getAccountId(cookie, headers); if (!accountId) return redirect("/dashboard"); return html("new", { nav: "monitors", scripts: ["/dashboard/query-builder.js"] }); }) // Home data endpoint for polling (monitor list change detection) .get("/dashboard/home/data", async ({ cookie, headers }) => { const accountId = await getAccountId(cookie, headers); 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" }, }); }) // Monitor detail — SSR with initial data .get("/dashboard/monitors/:id", async ({ cookie, headers, params }) => { const accountId = await getAccountId(cookie, headers); 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 100 `; return html("detail", { nav: "monitors", monitor, pings, scripts: ["/dashboard/query-builder.js"] }); }) // Chart partial endpoint — returns just the latency chart SVG .get("/dashboard/monitors/:id/chart", async ({ cookie, headers, params }) => { const accountId = await getAccountId(cookie, headers); 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 100 `; const chartPings = pings.slice().reverse(); return new Response(latencyChartSSR(chartPings), { headers: { "content-type": "text/html; charset=utf-8" }, }); }) // Sparkline partial — returns just the SVG for one monitor .get("/dashboard/monitors/:id/sparkline", async ({ cookie, headers, params }) => { const accountId = await getAccountId(cookie, headers); 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 FROM pings WHERE monitor_id = ${params.id} AND latency_ms IS NOT NULL ORDER BY checked_at DESC LIMIT 20 `; const latencies = pings.map((p: any) => p.latency_ms).reverse(); return new Response(sparklineSSR(latencies), { headers: { "content-type": "text/html; charset=utf-8" }, }); }) // Docs .get("/docs", () => Bun.file(`${dashDir}/docs.html`));