|
|
|
@@ -33,7 +33,6 @@ function latencyChartSSR(pings: any[]): string {
|
|
|
|
|
const pad = { top: 8, bottom: 8 };
|
|
|
|
|
const cH = h - pad.top - pad.bottom;
|
|
|
|
|
|
|
|
|
|
// Build ordered list of unique runs, evenly spaced (matches canvas)
|
|
|
|
|
const runTimes: Record<string, number[]> = {};
|
|
|
|
|
for (const p of data) {
|
|
|
|
|
const rid = p.run_id || p.checked_at;
|
|
|
|
@@ -49,7 +48,6 @@ function latencyChartSSR(pings: any[]): string {
|
|
|
|
|
runs.forEach((rid, i) => { runIndex[rid] = i; });
|
|
|
|
|
const maxIdx = Math.max(runs.length - 1, 1);
|
|
|
|
|
|
|
|
|
|
// Group by region
|
|
|
|
|
const byRegion: Record<string, any[]> = {};
|
|
|
|
|
for (const p of data) {
|
|
|
|
|
const key = p.region || '__none__';
|
|
|
|
@@ -71,7 +69,6 @@ function latencyChartSSR(pings: any[]): string {
|
|
|
|
|
return pad.top + cH - ((v - yMin) / yRange) * cH;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Grid lines
|
|
|
|
|
let grid = '';
|
|
|
|
|
for (let i = 0; i <= 4; i++) {
|
|
|
|
|
const y = (pad.top + (cH / 4) * i).toFixed(1);
|
|
|
|
@@ -152,15 +149,12 @@ const dashDir = resolve(import.meta.dir, "../dashboard");
|
|
|
|
|
export const dashboard = new Elysia()
|
|
|
|
|
.get("/", () => html("landing", {}))
|
|
|
|
|
|
|
|
|
|
// Shared assets
|
|
|
|
|
.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" } }))
|
|
|
|
|
// Dashboard-only assets
|
|
|
|
|
.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) {
|
|
|
|
@@ -172,21 +166,17 @@ export const dashboard = new Elysia()
|
|
|
|
|
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;
|
|
|
|
@@ -202,7 +192,6 @@ export const dashboard = new Elysia()
|
|
|
|
|
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<string, any[]> = {};
|
|
|
|
|
if (monitorIds.length > 0) {
|
|
|
|
@@ -226,7 +215,6 @@ export const dashboard = new Elysia()
|
|
|
|
|
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;
|
|
|
|
@@ -239,7 +227,6 @@ export const dashboard = new Elysia()
|
|
|
|
|
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`
|
|
|
|
@@ -255,7 +242,6 @@ export const dashboard = new Elysia()
|
|
|
|
|
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");
|
|
|
|
@@ -264,10 +250,8 @@ export const dashboard = new Elysia()
|
|
|
|
|
const hasLifetime = acc.plan === "lifetime" || stack.some((s: any) => s.plan === "lifetime");
|
|
|
|
|
if (acc.plan === "lifetime" && stack.length === 0) return redirect("/dashboard/settings");
|
|
|
|
|
|
|
|
|
|
// Total spent on paid invoices (for lifetime discount)
|
|
|
|
|
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'`;
|
|
|
|
|
|
|
|
|
|
// Fetch coins server-side for no-JS rendering
|
|
|
|
|
const payApi = process.env.PAY_API || "https://pay.pingql.com";
|
|
|
|
|
let coins: any[] = [];
|
|
|
|
|
try {
|
|
|
|
@@ -279,7 +263,6 @@ export const dashboard = new Elysia()
|
|
|
|
|
return html("checkout", { nav: "settings", account: acc, payApi, invoiceId: null, coins, invoice: null, totalSpent: Number(total_spent), hasLifetime });
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 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");
|
|
|
|
@@ -302,7 +285,6 @@ export const dashboard = new Elysia()
|
|
|
|
|
return html("checkout", { nav: "settings", account: acc, payApi, invoiceId: params.id, coins, invoice });
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Receipt (proxy to pay service, serves HTML directly)
|
|
|
|
|
.get("/dashboard/checkout/:id/receipt", async ({ cookie, headers, params, set }) => {
|
|
|
|
|
const resolved = await getAccountId(cookie, headers);
|
|
|
|
|
if (!resolved?.accountId) return redirect("/dashboard");
|
|
|
|
@@ -326,7 +308,6 @@ export const dashboard = new Elysia()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 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");
|
|
|
|
@@ -352,7 +333,6 @@ export const dashboard = new Elysia()
|
|
|
|
|
return redirect("/dashboard/checkout");
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// New monitor
|
|
|
|
|
.get("/dashboard/monitors/new", async ({ cookie, headers }) => {
|
|
|
|
|
const resolved = await getAccountId(cookie, headers);
|
|
|
|
|
const accountId = resolved?.accountId ?? null;
|
|
|
|
@@ -361,7 +341,6 @@ export const dashboard = new Elysia()
|
|
|
|
|
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;
|
|
|
|
@@ -376,7 +355,6 @@ export const dashboard = new Elysia()
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
@@ -396,7 +374,6 @@ export const dashboard = new Elysia()
|
|
|
|
|
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;
|
|
|
|
@@ -418,7 +395,6 @@ export const dashboard = new Elysia()
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
@@ -440,9 +416,6 @@ export const dashboard = new Elysia()
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// ── 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");
|
|
|
|
@@ -451,7 +424,6 @@ export const dashboard = new Elysia()
|
|
|
|
|
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> = {};
|
|
|
|
|
// 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++) {
|
|
|
|
@@ -481,7 +453,6 @@ export const dashboard = new Elysia()
|
|
|
|
|
return redirect("/dashboard/home");
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Edit monitor via form POST
|
|
|
|
|
.post("/dashboard/monitors/:id/edit", async ({ cookie, headers, params, body }) => {
|
|
|
|
|
const resolved = await getAccountId(cookie, headers);
|
|
|
|
|
if (!resolved?.accountId) return redirect("/dashboard");
|
|
|
|
@@ -519,7 +490,6 @@ export const dashboard = new Elysia()
|
|
|
|
|
return redirect(`/dashboard/monitors/${params.id}`);
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 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");
|
|
|
|
@@ -534,7 +504,6 @@ export const dashboard = new Elysia()
|
|
|
|
|
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");
|
|
|
|
@@ -549,7 +518,6 @@ export const dashboard = new Elysia()
|
|
|
|
|
return redirect(`/dashboard/monitors/${params.id}`);
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Docs
|
|
|
|
|
.get("/docs", () => html("docs", {}))
|
|
|
|
|
.get("/privacy", () => html("privacy", {}))
|
|
|
|
|
.get("/terms", () => html("tos", {}));
|
|
|
|
|