import { Elysia } from "elysia"; import { Eta } from "eta"; import { resolve } from "path"; import { resolveKey } from "./auth"; import sql from "../db"; import { sparkline, sparklineFromPings } from "../utils/sparkline"; import { createHash } from "crypto"; // Generate a cache-buster hash from the CSS file content at startup 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 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 = sparklineFromPings; const REGION_COLORS: Record = { 'eu-central': '#3b82f6', // blue 'us-west': '#f59e0b', // amber '__none__': '#6b7280', // gray for null region }; const REGION_LABELS: Record = { 'eu-central': 'πŸ‡©πŸ‡ͺ EU', 'us-west': 'πŸ‡ΊπŸ‡Έ US-W', '__none__': '?', }; function latencyChartSSR(pings: any[]): string { const data = pings.filter((c: any) => c.latency_ms != null); if (data.length < 2) { return '
Not enough data
'; } // Group by region 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 max = Math.max(...allValues, 1); const min = Math.min(...allValues, 0); const range = max - min || 1; const w = 800; const h = 128; // Find the overall time range to align lines on a shared x-axis const allTimes = data.map((c: any) => new Date(c.checked_at).getTime()); const tMin = Math.min(...allTimes); const tMax = Math.max(...allTimes); const tRange = tMax - tMin || 1; 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) => new Date(a.checked_at).getTime() - new Date(b.checked_at).getTime() ); const points = rPings.map((p: any) => { const x = ((new Date(p.checked_at).getTime() - tMin) / tRange) * w; const y = h - ((p.latency_ms - min) / range) * (h - 16) - 8; return { x, y, up: p.up }; }); if (points.length < 1) continue; const pathD = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' '); paths += ``; dots += points.filter(p => !p.up).map(p => `` ).join(''); legend += `${label}`; } return `
${paths}${dots} ${max}ms ${min}ms ${regions.length > 1 ? `
${legend}
` : ''}
`; } function escapeHtmlSSR(str: string): string { return str.replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } export function html(template: string, data: Record = {}) { return new Response(eta.render(template, { ...data, timeAgoSSR, sparklineSSR, latencyChartSSR, escapeHtmlSSR, cssHash, jsHash }), { headers: { "content-type": "text/html; charset=utf-8" }, }); } function redirect(to: string) { return new Response( ``, { headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" } }, ); } async function getAccountId(cookie: any, headers: any): Promise<{ accountId: string; keyId: string | null; plan: string } | null> { const authHeader = headers["authorization"] ?? ""; const bearer = authHeader.match(/^bearer\s+(.+)$/i)?.[1]?.trim(); const key = cookie?.pingql_key?.value || bearer; if (!key) return null; return await resolveKey(key) ?? null; } const dashDir = resolve(import.meta.dir, "../dashboard"); export const dashboard = new Elysia() .get("/", () => html("landing", {})) .get("/dashboard/app.js", () => new Response(Bun.file(`${dashDir}/app.js`), { headers: { "cache-control": "public, max-age=31536000, immutable" } })) .get("/dashboard/app.css", () => new Response(Bun.file(`${dashDir}/app.css`), { headers: { "cache-control": "public, max-age=31536000, immutable" } })) .get("/dashboard/tailwind.css", () => new Response(Bun.file(`${dashDir}/tailwind.css`), { 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" } })) // 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 html("login", {}); }) // Logout .get("/dashboard/logout", ({ cookie }) => { // Explicitly expire with same domain/path so browser actually clears it 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"); }) // Welcome page β€” shows new account key after registration (no-JS flow) .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 || "" }); }) // Home β€” SSR monitor list .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 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, COALESCE(p.region,'__none__') 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 resolved = await getAccountId(cookie, headers); const accountId = resolved?.accountId ?? null; const keyId = resolved?.keyId ?? null; if (!accountId) return redirect("/dashboard"); const [acc] = await sql`SELECT id, email_hash, plan, plan_expires_at, created_at FROM accounts WHERE id = ${accountId}`; const isSubKey = !!keyId; const apiKeys = isSubKey ? [] : 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 = isSubKey ? null : (cookie?.pingql_key?.value ?? null); const [{ count: monitorCount }] = await sql`SELECT COUNT(*)::int as count FROM monitors WHERE account_id = ${accountId}`; // Fetch paid + active (non-expired) invoices let invoices: any[] = []; try { invoices = await 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 {} return html("settings", { nav: "settings", account: acc, apiKeys, accountId, loginKey, isSubKey, monitorCount, invoices }); }) // Checkout β€” upgrade plan .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 FROM accounts WHERE id = ${resolved.accountId}`; if (acc.plan === "lifetime") return redirect("/dashboard/settings"); // Fetch coins server-side for no-JS rendering 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 }); }) // Existing invoice by ID β€” SSR the payment status .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 }); }) // Create checkout via form POST (no-JS) .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"); }) // New monitor .get("/dashboard/monitors/new", async ({ cookie, headers }) => { const resolved = await getAccountId(cookie, headers); const accountId = resolved?.accountId ?? null; const keyId = resolved?.keyId ?? null; if (!accountId) return redirect("/dashboard"); return html("new", { nav: "monitors", plan: resolved?.plan || "free" }); }) // Home data endpoint for polling (monitor list change detection) .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" }, }); }) // Monitor detail β€” SSR with initial data .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 100 `; return html("detail", { nav: "monitors", monitor, pings, plan: resolved?.plan || "free" }); }) // Chart partial endpoint β€” returns just the latency chart SVG .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 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 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" }, }); }) // ── Form-based monitor actions (no-JS support) ───────────────────── // Create monitor via form POST .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 = {}; // Collect header_key[]/header_value[] pairs 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, regions, request_headers: Object.keys(requestHeaders).length ? requestHeaders : null, request_body: b.request_body || null, query, }), }); } catch {} return redirect("/dashboard/home"); }) // Delete monitor via form POST .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"); }) // Toggle monitor via form POST .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}`); }) // Docs .get("/docs", () => html("docs", {})) .get("/privacy", () => html("privacy", {})) .get("/terms", () => html("tos", {}));