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 `
${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", {}));