my own status page

feat: add timeout and favicon

dunkirk.sh 50d3fe0f 63581681

verified
+65 -9
+1 -1
migrations/0002_add_statuses.sql
··· 4 4 id INTEGER PRIMARY KEY AUTOINCREMENT, 5 5 service_id TEXT NOT NULL, 6 6 timestamp INTEGER NOT NULL, 7 - status TEXT NOT NULL CHECK (status IN ('up', 'degraded', 'misconfigured', 'down', 'unknown')), 7 + status TEXT NOT NULL CHECK (status IN ('up', 'degraded', 'misconfigured', 'timeout', 'down', 'unknown')), 8 8 latency_ms INTEGER 9 9 ); 10 10
+5 -3
src/health.ts
··· 2 2 3 3 const SLOW_THRESHOLD_MS = 3000; 4 4 5 - export type Status = "up" | "degraded" | "misconfigured" | "down" | "unknown"; 5 + export type Status = "up" | "degraded" | "misconfigured" | "timeout" | "down" | "unknown"; 6 6 7 7 interface HealthResult { 8 8 status: Status; ··· 36 36 return { status: "up", latency_ms }; 37 37 } 38 38 return { status: "down", latency_ms }; 39 - } catch { 40 - return { status: "down", latency_ms: Date.now() - start }; 39 + } catch (err) { 40 + const latency_ms = Date.now() - start; 41 + const isTimeout = err instanceof DOMException && err.name === "TimeoutError"; 42 + return { status: isTimeout ? "timeout" : "down", latency_ms }; 41 43 } 42 44 }
+5
src/index.ts
··· 4 4 import { insertPing, pruneOldPings } from "./db"; 5 5 import { refreshDevices } from "./tailscale"; 6 6 import { handleStatusRoute } from "./routes/status"; 7 + import { handleFavicon } from "./routes/favicon"; 7 8 import { handleUptime } from "./routes/uptime"; 8 9 import { handleBadgeRoute } from "./routes/badge"; 9 10 import { handleIndex } from "./routes/index"; ··· 15 16 16 17 if (path === "/" || path === "") { 17 18 return handleIndex(env); 19 + } 20 + 21 + if (path === "/favicon.svg") { 22 + return handleFavicon(env); 18 23 } 19 24 20 25 if (path === "/health") {
+4 -2
src/routes/badge.ts
··· 7 7 up: "#3cc068", 8 8 degraded: "#f0ad4e", 9 9 misconfigured: "#9b59b6", 10 + timeout: "#e05d44", 10 11 partial: "#f0ad4e", 11 12 down: "#e05d44", 12 13 unknown: "#9f9f9f", ··· 16 17 up: "operational", 17 18 degraded: "degraded", 18 19 misconfigured: "misconfigured", 20 + timeout: "timeout", 19 21 partial: "partial", 20 22 down: "down", 21 23 unknown: "unknown", ··· 154 156 155 157 function worstStatus(statuses: string[]): string { 156 158 if (statuses.length === 0) return "unknown"; 157 - if (statuses.every((s) => s === "down")) return "down"; 158 - if (statuses.includes("down")) return "partial"; 159 + if (statuses.every((s) => s === "down" || s === "timeout")) return "down"; 160 + if (statuses.includes("down") || statuses.includes("timeout")) return "partial"; 159 161 if (statuses.includes("misconfigured")) return "misconfigured"; 160 162 if (statuses.includes("degraded")) return "degraded"; 161 163 if (statuses.includes("unknown")) return "unknown";
+44
src/routes/favicon.ts
··· 1 + import type { Env } from "../types"; 2 + import { getManifest } from "../manifest"; 3 + import { getLatestPing } from "../db"; 4 + 5 + const COLORS: Record<string, string> = { 6 + up: "#2ecc71", 7 + degraded: "#f39c12", 8 + down: "#e74c3c", 9 + }; 10 + 11 + export async function handleFavicon(env: Env): Promise<Response> { 12 + const manifest = await getManifest(env); 13 + const activeServers = Object.values(manifest).filter( 14 + (m) => m.type === "server" && m.services.length > 0, 15 + ); 16 + const statuses: string[] = []; 17 + 18 + for (const machine of activeServers) { 19 + for (const svc of machine.services.filter((s) => s.health_url)) { 20 + const ping = await getLatestPing(env.DB, svc.name); 21 + statuses.push((ping?.status as string) ?? "unknown"); 22 + } 23 + } 24 + 25 + const downCount = statuses.filter((s) => s === "down" || s === "timeout").length; 26 + const downRatio = statuses.length > 0 ? downCount / statuses.length : 0; 27 + const hasIssues = statuses.some( 28 + (s) => s === "down" || s === "timeout" || s === "degraded" || s === "misconfigured", 29 + ); 30 + 31 + let color: string; 32 + if (downRatio >= 0.4) color = COLORS.down; 33 + else if (hasIssues) color = COLORS.degraded; 34 + else color = COLORS.up; 35 + 36 + const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><circle cx="8" cy="8" r="7" fill="${color}"/></svg>`; 37 + 38 + return new Response(svg, { 39 + headers: { 40 + "Content-Type": "image/svg+xml", 41 + "Cache-Control": "no-cache, no-store, must-revalidate", 42 + }, 43 + }); 44 + }
+4 -1
src/routes/index.ts
··· 40 40 const activeServers = servers.filter((m) => m.services.length > 0); 41 41 const anyServerOffline = activeServers.some((m) => !m.online); 42 42 const svcStatuses = activeServers.flatMap((m) => m.services.map((s) => s.status)); 43 - const downCount = svcStatuses.filter((s) => s === "down").length; 43 + const downCount = svcStatuses.filter((s) => s === "down" || s === "timeout").length; 44 44 const downRatio = svcStatuses.length > 0 ? downCount / svcStatuses.length : 0; 45 45 const onFire = anyServerOffline || downRatio >= 0.4; 46 46 const hasDegraded = 47 47 svcStatuses.includes("down") || 48 + svcStatuses.includes("timeout") || 48 49 svcStatuses.includes("degraded") || 49 50 svcStatuses.includes("misconfigured") || 50 51 svcStatuses.includes("partial"); ··· 61 62 <meta charset="utf-8"> 62 63 <meta name="viewport" content="width=device-width, initial-scale=1"> 63 64 <title>infra.dunkirk.sh</title> 65 + <link rel="icon" type="image/svg+xml" href="/favicon.svg"> 64 66 <style> 65 67 * { margin: 0; padding: 0; box-sizing: border-box; } 66 68 html { min-height: 100%; } ··· 72 74 .dot.degraded { background: #f39c12; } 73 75 .dot.down { background: #e74c3c; } 74 76 .dot.misconfigured { background: #9b59b6; } 77 + .dot.timeout { background: #e74c3c; } 75 78 .dot.unknown { background: #8b949e; } 76 79 .dot.online { background: #2ecc71; } 77 80 .dot.offline { background: #e74c3c; }
+2 -2
src/routes/status.ts
··· 7 7 8 8 function worstStatus(statuses: string[]): string { 9 9 if (statuses.length === 0) return "unknown"; 10 - if (statuses.every((s) => s === "down")) return "down"; 11 - if (statuses.includes("down")) return "partial"; 10 + if (statuses.every((s) => s === "down" || s === "timeout")) return "down"; 11 + if (statuses.includes("down") || statuses.includes("timeout")) return "partial"; 12 12 if (statuses.includes("misconfigured")) return "misconfigured"; 13 13 if (statuses.includes("degraded")) return "degraded"; 14 14 if (statuses.includes("unknown")) return "unknown";