my own status page
0
fork

Configure Feed

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

feat: optimize queries

+162 -70
+2
migrations/0005_add_indexes.sql
··· 1 + CREATE INDEX idx_pings_timestamp ON pings (timestamp); 2 + CREATE INDEX idx_incidents_github ON incidents (github_repo, github_issue_number);
+91 -19
src/db.ts
··· 31 31 service_id: string, 32 32 ): Promise<number> { 33 33 const since = Math.floor(Date.now() / 1000) - 7 * 24 * 60 * 60; 34 - const total = await db 34 + const row = await db 35 35 .prepare( 36 - "SELECT COUNT(*) as count FROM pings WHERE service_id = ? AND timestamp >= ?", 36 + "SELECT COUNT(*) as total, SUM(CASE WHEN status = 'up' THEN 1 ELSE 0 END) as up_count FROM pings WHERE service_id = ? AND timestamp >= ?", 37 37 ) 38 38 .bind(service_id, since) 39 - .first<{ count: number }>(); 40 - const up = await db 39 + .first<{ total: number; up_count: number }>(); 40 + 41 + if (!row || row.total === 0) return 100; 42 + return Math.round((row.up_count / row.total) * 10000) / 100; 43 + } 44 + 45 + export async function getAllLatestPings( 46 + db: D1Database, 47 + ): Promise<Map<string, { status: string; latency_ms: number | null }>> { 48 + const rows = await db 49 + .prepare( 50 + `SELECT p.service_id, p.status, p.latency_ms 51 + FROM pings p 52 + INNER JOIN (SELECT service_id, MAX(timestamp) as max_ts FROM pings GROUP BY service_id) latest 53 + ON p.service_id = latest.service_id AND p.timestamp = latest.max_ts`, 54 + ) 55 + .all(); 56 + 57 + const map = new Map<string, { status: string; latency_ms: number | null }>(); 58 + for (const row of rows.results) { 59 + map.set(row.service_id as string, { 60 + status: row.status as string, 61 + latency_ms: row.latency_ms as number | null, 62 + }); 63 + } 64 + return map; 65 + } 66 + 67 + export async function getAllUptime7d( 68 + db: D1Database, 69 + ): Promise<Map<string, number>> { 70 + const since = Math.floor(Date.now() / 1000) - 7 * 24 * 60 * 60; 71 + const rows = await db 41 72 .prepare( 42 - "SELECT COUNT(*) as count FROM pings WHERE service_id = ? AND timestamp >= ? AND status = 'up'", 73 + `SELECT service_id, COUNT(*) as total, SUM(CASE WHEN status = 'up' THEN 1 ELSE 0 END) as up_count 74 + FROM pings WHERE timestamp >= ? 75 + GROUP BY service_id`, 43 76 ) 44 - .bind(service_id, since) 45 - .first<{ count: number }>(); 77 + .bind(since) 78 + .all(); 46 79 47 - if (!total || total.count === 0) return 100; 48 - return Math.round(((up?.count ?? 0) / total.count) * 10000) / 100; 80 + const map = new Map<string, number>(); 81 + for (const row of rows.results) { 82 + const total = row.total as number; 83 + const up = row.up_count as number; 84 + map.set( 85 + row.service_id as string, 86 + total === 0 ? 100 : Math.round((up / total) * 10000) / 100, 87 + ); 88 + } 89 + return map; 49 90 } 50 91 51 92 export async function getUptimeBuckets( ··· 220 261 } 221 262 222 263 export async function getActiveIncidentsWithUpdates(db: D1Database): Promise<IncidentWithUpdates[]> { 223 - const incidents = await getActiveIncidents(db); 224 - return Promise.all( 225 - incidents.map(async (incident) => { 226 - const updates = await db 227 - .prepare("SELECT * FROM incident_updates WHERE incident_id = ? ORDER BY created_at ASC") 228 - .bind(incident.id) 229 - .all(); 230 - return { ...incident, updates: updates.results as unknown as IncidentUpdate[] }; 231 - }), 232 - ); 264 + const rows = await db 265 + .prepare( 266 + `SELECT i.*, u.id as update_id, u.status as update_status, u.message as update_message, u.created_at as update_created_at 267 + FROM incidents i 268 + LEFT JOIN incident_updates u ON u.incident_id = i.id 269 + WHERE i.status != 'resolved' 270 + ORDER BY i.created_at DESC, u.created_at ASC`, 271 + ) 272 + .all(); 273 + 274 + const incidentMap = new Map<number, IncidentWithUpdates>(); 275 + for (const row of rows.results) { 276 + const id = row.id as number; 277 + if (!incidentMap.has(id)) { 278 + incidentMap.set(id, { 279 + id, 280 + service_id: row.service_id as string, 281 + title: row.title as string, 282 + status: row.status as string, 283 + severity: row.severity as string, 284 + triage_report: row.triage_report as string | null, 285 + github_repo: row.github_repo as string | null, 286 + github_issue_number: row.github_issue_number as number | null, 287 + started_at: row.started_at as number, 288 + resolved_at: row.resolved_at as number | null, 289 + created_at: row.created_at as number, 290 + updated_at: row.updated_at as number, 291 + updates: [], 292 + }); 293 + } 294 + if (row.update_id) { 295 + incidentMap.get(id)!.updates.push({ 296 + id: row.update_id as number, 297 + incident_id: id, 298 + status: row.update_status as string, 299 + message: row.update_message as string, 300 + created_at: row.update_created_at as number, 301 + }); 302 + } 303 + } 304 + return Array.from(incidentMap.values()); 233 305 } 234 306 235 307 export async function getActiveIncidentForService(
+19 -9
src/overall.ts
··· 1 - import type { Env } from "./types"; 1 + import type { Env, Incident, ServicesManifest } from "./types"; 2 2 import { getManifest } from "./manifest"; 3 - import { getLatestPing, getActiveIncidents } from "./db"; 3 + import { getAllLatestPings, getActiveIncidents } from "./db"; 4 4 import { getDeviceStatus } from "./tailscale"; 5 5 6 6 export type OverallGrade = "up" | "degraded" | "down"; ··· 10 10 label: string; 11 11 } 12 12 13 - export async function getOverallStatus(env: Env): Promise<OverallStatus> { 14 - const manifest = await getManifest(env); 13 + export async function getOverallStatus( 14 + env: Env, 15 + prefetched?: { 16 + manifest?: ServicesManifest; 17 + latestPings?: Map<string, { status: string; latency_ms: number | null }>; 18 + activeIncidents?: Incident[]; 19 + machineOnline?: Map<string, boolean>; 20 + }, 21 + ): Promise<OverallStatus> { 22 + const manifest = prefetched?.manifest ?? await getManifest(env); 23 + const latestPings = prefetched?.latestPings ?? await getAllLatestPings(env.DB); 24 + const activeIncidents = prefetched?.activeIncidents ?? await getActiveIncidents(env.DB); 25 + 15 26 const servers = Object.entries(manifest).filter( 16 27 ([, m]) => m.type === "server" && m.services.length > 0, 17 28 ); ··· 19 30 const statuses: string[] = []; 20 31 let anyServerOffline = false; 21 32 22 - for (const [, machine] of servers) { 23 - const online = await getDeviceStatus(env, machine.tailscale_host); 33 + for (const [name, machine] of servers) { 34 + const online = prefetched?.machineOnline?.get(name) ?? await getDeviceStatus(env, machine.tailscale_host); 24 35 if (!online) anyServerOffline = true; 25 36 26 37 for (const svc of machine.services.filter((s) => s.health_url)) { 27 - const ping = await getLatestPing(env.DB, svc.name); 28 - statuses.push((ping?.status as string) ?? "unknown"); 38 + const ping = latestPings.get(svc.name); 39 + statuses.push(ping?.status ?? "unknown"); 29 40 } 30 41 } 31 42 ··· 42 53 s === "misconfigured", 43 54 ); 44 55 45 - const activeIncidents = await getActiveIncidents(env.DB); 46 56 const hasCritical = activeIncidents.some((i) => i.severity === "critical"); 47 57 const hasMajor = activeIncidents.some((i) => i.severity === "major"); 48 58
+26 -22
src/routes/index.ts
··· 1 1 import type { Env } from "../types"; 2 2 import { getManifest } from "../manifest"; 3 - import { getLatestPing, getUptime7d, getOverallUptimeDays, getLastCheckTime, getActiveIncidentsWithUpdates, getRecentIncidents } from "../db"; 3 + import { getAllLatestPings, getAllUptime7d, getOverallUptimeDays, getLastCheckTime, getActiveIncidentsWithUpdates, getActiveIncidents, getRecentIncidents } from "../db"; 4 4 import { getDeviceStatus } from "../tailscale"; 5 5 import { getOverallStatus } from "../overall"; 6 6 import { COMMIT_SHA } from "../version"; 7 7 8 8 export async function handleIndex(env: Env): Promise<Response> { 9 - const manifest = await getManifest(env); 9 + const [manifest, latestPings, uptimes, lastCheck, uptimeDays, activeIncidentsWithUpdates, activeIncidentsList, recentIncidents] = await Promise.all([ 10 + getManifest(env), 11 + getAllLatestPings(env.DB), 12 + getAllUptime7d(env.DB), 13 + getLastCheckTime(env.DB), 14 + getOverallUptimeDays(env.DB, 90), 15 + getActiveIncidentsWithUpdates(env.DB), 16 + getActiveIncidents(env.DB), 17 + getRecentIncidents(env.DB, 7), 18 + ]); 10 19 20 + const machineOnline = new Map<string, boolean>(); 11 21 const machines = await Promise.all( 12 22 Object.entries(manifest).map(async ([name, machine]) => { 13 23 const online = await getDeviceStatus(env, machine.tailscale_host); 14 - const services = await Promise.all( 15 - machine.services.map(async (svc) => { 16 - const ping = await getLatestPing(env.DB, svc.name); 17 - const uptime = await getUptime7d(env.DB, svc.name); 18 - return { 19 - name: svc.name, 20 - description: svc.description, 21 - url: `https://${svc.domain}`, 22 - status: ping?.status ?? "unknown", 23 - latency_ms: ping?.latency_ms ?? null, 24 - uptime_7d: uptime, 25 - has_health: svc.health_url !== null, 26 - }; 27 - }), 28 - ); 24 + machineOnline.set(name, online); 25 + const services = machine.services.map((svc) => ({ 26 + name: svc.name, 27 + description: svc.description, 28 + url: `https://${svc.domain}`, 29 + status: latestPings.get(svc.name)?.status ?? "unknown", 30 + latency_ms: latestPings.get(svc.name)?.latency_ms ?? null, 31 + uptime_7d: uptimes.get(svc.name) ?? 100, 32 + has_health: svc.health_url !== null, 33 + })); 29 34 return { name, type: machine.type, online, services }; 30 35 }), 31 36 ); 32 37 33 - const lastCheck = await getLastCheckTime(env.DB); 34 38 const lastCheckISO = lastCheck 35 39 ? new Date(lastCheck * 1000).toISOString() 36 40 : null; ··· 38 42 const servers = machines.filter((m) => m.type === "server"); 39 43 const clients = machines.filter((m) => m.type === "client"); 40 44 41 - const { grade: overallClass, label: overallText } = await getOverallStatus(env); 42 - const uptimeDays = await getOverallUptimeDays(env.DB, 90); 43 - const activeIncidents = await getActiveIncidentsWithUpdates(env.DB); 44 - const recentIncidents = await getRecentIncidents(env.DB, 7); 45 + const { grade: overallClass, label: overallText } = await getOverallStatus(env, { 46 + manifest, latestPings, activeIncidents: activeIncidentsList, machineOnline, 47 + }); 48 + const activeIncidents = activeIncidentsWithUpdates; 45 49 const resolvedIncidents = recentIncidents.filter((i) => i.status === "resolved"); 46 50 47 51 const html = `<!DOCTYPE html>
+24 -20
src/routes/status.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, getAllLatestPings, getAllUptime7d, getLastCheckTime } from "../db"; 4 4 import { getDeviceStatus } from "../tailscale"; 5 5 import { getOverallStatus } from "../overall"; 6 6 ··· 16 16 17 17 // GET /api/status/overall 18 18 async function overallStatus(env: Env): Promise<Response> { 19 - const manifest = await getManifest(env); 19 + const [manifest, uptimes, latestPings] = await Promise.all([ 20 + getManifest(env), 21 + getAllUptime7d(env.DB), 22 + getAllLatestPings(env.DB), 23 + ]); 20 24 const allServices = Object.values(manifest).flatMap((m) => m.services); 21 25 const monitored = allServices.filter((s) => s.health_url !== null); 22 26 23 27 let totalUptime = 0; 24 28 for (const svc of monitored) { 25 - const uptime = await getUptime7d(env.DB, svc.name); 26 - totalUptime += uptime; 29 + totalUptime += uptimes.get(svc.name) ?? 100; 27 30 } 28 31 29 - const { grade } = await getOverallStatus(env); 32 + const { grade } = await getOverallStatus(env, { manifest, latestPings }); 30 33 const avgUptime = 31 34 monitored.length > 0 32 35 ? Math.round((totalUptime / monitored.length) * 100) / 100 ··· 45 48 46 49 // GET /api/status 47 50 async function fullStatus(env: Env): Promise<Response> { 48 - const manifest = await getManifest(env); 51 + const [manifest, latestPings, uptimes, lastCheck] = await Promise.all([ 52 + getManifest(env), 53 + getAllLatestPings(env.DB), 54 + getAllUptime7d(env.DB), 55 + getLastCheckTime(env.DB), 56 + ]); 49 57 50 58 const machines = await Promise.all( 51 59 Object.entries(manifest).map(async ([name, machine]) => { 52 60 const online = await getDeviceStatus(env, machine.tailscale_host); 53 - const services = await Promise.all( 54 - machine.services.map(async (svc) => { 55 - const ping = await getLatestPing(env.DB, svc.name); 56 - const uptime = await getUptime7d(env.DB, svc.name); 57 - return { 58 - id: svc.name, 59 - status: (ping?.status ?? "unknown") as string, 60 - latency_ms: ping?.latency_ms ?? null, 61 - uptime_7d: uptime, 62 - }; 63 - }), 64 - ); 61 + const services = machine.services.map((svc) => { 62 + const ping = latestPings.get(svc.name); 63 + return { 64 + id: svc.name, 65 + status: (ping?.status ?? "unknown") as string, 66 + latency_ms: ping?.latency_ms ?? null, 67 + uptime_7d: uptimes.get(svc.name) ?? 100, 68 + }; 69 + }); 65 70 const svcStatuses = services.map((s) => s.status); 66 71 return { 67 72 name, ··· 76 81 }), 77 82 ); 78 83 79 - const { grade } = await getOverallStatus(env); 80 - const lastCheck = await getLastCheckTime(env.DB); 84 + const { grade } = await getOverallStatus(env, { manifest, latestPings }); 81 85 82 86 return Response.json( 83 87 {