my own status page
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add top bar

+65 -3
+51
src/db.ts
··· 86 86 return result; 87 87 } 88 88 89 + export async function getOverallUptimeDays( 90 + db: D1Database, 91 + days: number, 92 + ): Promise<{ date: string; status: "up" | "degraded" | "down" | "none" }[]> { 93 + const since = Math.floor(Date.now() / 1000) - days * 24 * 60 * 60; 94 + const rows = await db 95 + .prepare( 96 + `SELECT 97 + (timestamp / 86400) AS day_bucket, 98 + status, 99 + COUNT(*) AS cnt 100 + FROM pings 101 + WHERE timestamp >= ? 102 + GROUP BY day_bucket, status 103 + ORDER BY day_bucket ASC`, 104 + ) 105 + .bind(since) 106 + .all(); 107 + 108 + const bucketMap = new Map<number, Map<string, number>>(); 109 + for (const row of rows.results) { 110 + const b = row.day_bucket as number; 111 + if (!bucketMap.has(b)) bucketMap.set(b, new Map()); 112 + bucketMap.get(b)!.set(row.status as string, row.cnt as number); 113 + } 114 + 115 + const now = Math.floor(Date.now() / 1000); 116 + const todayBucket = Math.floor(now / 86400); 117 + const result: { date: string; status: "up" | "degraded" | "down" | "none" }[] = []; 118 + 119 + for (let i = days - 1; i >= 0; i--) { 120 + const bucket = todayBucket - i; 121 + const d = new Date(bucket * 86400 * 1000); 122 + const date = d.toISOString().slice(0, 10); 123 + const counts = bucketMap.get(bucket); 124 + 125 + if (!counts) { 126 + result.push({ date, status: "none" }); 127 + continue; 128 + } 129 + 130 + let status: "up" | "degraded" | "down" = "up"; 131 + if (counts.has("down") || counts.has("timeout")) status = "down"; 132 + else if (counts.has("degraded") || counts.has("misconfigured")) status = "degraded"; 133 + 134 + result.push({ date, status }); 135 + } 136 + 137 + return result; 138 + } 139 + 89 140 export async function getLastCheckTime( 90 141 db: D1Database, 91 142 ): Promise<number | null> {
+14 -3
src/routes/index.ts
··· 1 1 import type { Env } from "../types"; 2 2 import { getManifest } from "../manifest"; 3 - import { getLatestPing, getUptime7d, getLastCheckTime } from "../db"; 3 + import { getLatestPing, getUptime7d, getOverallUptimeDays, getLastCheckTime } from "../db"; 4 4 import { getDeviceStatus } from "../tailscale"; 5 5 import { getOverallStatus } from "../overall"; 6 6 import { COMMIT_SHA } from "../version"; ··· 39 39 const clients = machines.filter((m) => m.type === "client"); 40 40 41 41 const { grade: overallClass, label: overallText } = await getOverallStatus(env); 42 + const uptimeDays = await getOverallUptimeDays(env.DB, 90); 42 43 43 44 const html = `<!DOCTYPE html> 44 45 <html lang="en"> ··· 90 91 .clients-header { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.08em; color: #8b949e; margin-bottom: 0.5rem; } 91 92 .clients-list { display: flex; flex-wrap: wrap; gap: 0.5rem; } 92 93 .client { display: flex; align-items: center; gap: 0.25rem; font-size: 0.8rem; color: #8b949e; } 93 - footer { margin-top: auto; padding-top: 1rem; border-top: 1px solid #21262d; font-size: 0.7rem; color: #8b949e; display: flex; justify-content: space-between; } 94 + .uptime-bar { display: flex; position: fixed; top: 0; left: 0; right: 0; height: 3px; z-index: 10; } 95 + .uptime-bar .day { flex: 1; } 96 + .uptime-bar .day.up { background: #2ecc71; } 97 + .uptime-bar .day.degraded { background: #f39c12; } 98 + .uptime-bar .day.down { background: #e74c3c; } 99 + .uptime-bar .day.none { background: #21262d; } 100 + footer { margin-top: auto; padding-top: 1rem; border-top: 1px solid #21262d; font-size: 0.7rem; color: #8b949e; } 101 + .footer-meta { display: flex; justify-content: space-between; } 94 102 footer a { color: #8b949e; text-decoration: none; } 95 103 footer a:hover { text-decoration: underline; } 96 104 </style> 97 105 </head> 98 106 <body> 107 + <div class="uptime-bar">${uptimeDays.map((d) => `<div class="day ${d.status}" title="${d.date}: ${d.status}"></div>`).join("")}</div> 99 108 <h1>infra.dunkirk.sh</h1> 100 109 <p class="overall"><span class="dot ${overallClass}" id="overall-dot" title="${overallClass}"></span><span id="overall-text">${overallText}</span></p> 101 110 ${servers ··· 124 133 ${clients.map((m) => `<span class="client"><span class="dot ${m.online ? "online" : "unknown"}" data-machine="${esc(m.name)}" title="${m.online ? "online" : "offline"}"></span>${esc(m.name)}</span>`).join("\n")} 125 134 </div> 126 135 </div>` : ""} 127 - <footer><span>${lastCheckISO ? `updated <relative-time datetime="${lastCheckISO}" prefix="">loading</relative-time>` : "no checks yet"}</span><a href="https://github.com/taciturnaxolotl/status/commit/${COMMIT_SHA}">${COMMIT_SHA}</a></footer> 136 + <footer> 137 + <div class="footer-meta"><span>${lastCheckISO ? `updated <relative-time datetime="${lastCheckISO}" prefix="">loading</relative-time>` : "no checks yet"}</span><a href="https://github.com/taciturnaxolotl/status/commit/${COMMIT_SHA}">${COMMIT_SHA}</a></div> 138 + </footer> 128 139 <script> 129 140 class RelativeTimeElement extends HTMLElement { 130 141 static get observedAttributes() { return ['datetime']; }