my own status page
0
fork

Configure Feed

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

feat: add json schemas

+199 -41
+55 -24
src/index.ts
··· 8 8 import { handleUptime } from "./routes/uptime"; 9 9 import { handleBadgeRoute } from "./routes/badge"; 10 10 import { handleIndex } from "./routes/index"; 11 + import { schemas } from "./schemas"; 11 12 12 - export default { 13 - async fetch(request: Request, env: Env): Promise<Response> { 14 - const url = new URL(request.url); 15 - const path = url.pathname; 13 + async function handleRequest(request: Request, env: Env): Promise<Response> { 14 + const url = new URL(request.url); 15 + const path = url.pathname; 16 16 17 - if (path === "/" || path === "") { 18 - return handleIndex(env); 19 - } 17 + if (path === "/" || path === "") { 18 + return handleIndex(env); 19 + } 20 20 21 - if (path === "/favicon.svg") { 22 - return handleFavicon(env); 21 + if (path === "/favicon.svg") { 22 + return handleFavicon(env); 23 + } 24 + 25 + if (path === "/health") { 26 + return Response.json({ ok: true, timestamp: new Date().toISOString() }); 27 + } 28 + 29 + if (path === "/api/schemas") { 30 + return Response.json(schemas); 31 + } 32 + 33 + const schemaMatch = path.match(/^\/api\/schemas\/(.+)$/); 34 + if (schemaMatch) { 35 + const schema = schemas[schemaMatch[1]]; 36 + if (schema) { 37 + return Response.json(schema); 23 38 } 39 + return Response.json({ error: "schema not found" }, { status: 404 }); 40 + } 41 + 42 + if (path.startsWith("/api/status")) { 43 + const res = await handleStatusRoute(env, path); 44 + if (res) return res; 45 + } 24 46 25 - if (path === "/health") { 26 - return Response.json({ ok: true, timestamp: new Date().toISOString() }); 27 - } 47 + const uptimeMatch = path.match(/^\/api\/uptime\/(.+)$/); 48 + if (uptimeMatch) { 49 + return handleUptime(env, uptimeMatch[1], url); 50 + } 28 51 29 - if (path.startsWith("/api/status")) { 30 - const res = await handleStatusRoute(env, path); 31 - if (res) return res; 32 - } 52 + if (path.startsWith("/badge")) { 53 + const badge = await handleBadgeRoute(env, path, url); 54 + if (badge) return badge; 55 + } 33 56 34 - const uptimeMatch = path.match(/^\/api\/uptime\/(.+)$/); 35 - if (uptimeMatch) { 36 - return handleUptime(env, uptimeMatch[1], url); 37 - } 57 + return new Response("Not Found", { status: 404 }); 58 + } 38 59 39 - if (path.startsWith("/badge")) { 40 - const badge = await handleBadgeRoute(env, path, url); 41 - if (badge) return badge; 60 + export default { 61 + async fetch(request: Request, env: Env): Promise<Response> { 62 + if (request.method === "OPTIONS") { 63 + return new Response(null, { 64 + headers: { 65 + "Access-Control-Allow-Origin": "*", 66 + "Access-Control-Allow-Methods": "GET, OPTIONS", 67 + "Access-Control-Allow-Headers": "Content-Type", 68 + }, 69 + }); 42 70 } 43 71 44 - return new Response("Not Found", { status: 404 }); 72 + const response = await handleRequest(request, env); 73 + const corsResponse = new Response(response.body, response); 74 + corsResponse.headers.set("Access-Control-Allow-Origin", "*"); 75 + return corsResponse; 45 76 }, 46 77 47 78 async scheduled(_controller: ScheduledController, env: Env): Promise<void> {
+6 -16
src/routes/status.ts
··· 3 3 import { getLatestPing, getUptime7d } from "../db"; 4 4 import { getDeviceStatus } from "../tailscale"; 5 5 6 - const JSON_HEADERS = { "Access-Control-Allow-Origin": "*" }; 7 - 8 6 function worstStatus(statuses: string[]): string { 9 7 if (statuses.length === 0) return "unknown"; 10 8 if (statuses.every((s) => s === "down" || s === "timeout")) return "down"; ··· 45 43 services_total: allServices.length, 46 44 services_monitored: monitored.length, 47 45 machines_total: Object.keys(manifest).length, 48 - }, 49 - { headers: JSON_HEADERS }, 50 - ); 46 + } ); 51 47 } 52 48 53 49 // GET /api/status ··· 93 89 ok: status === "up", 94 90 status, 95 91 machines, 96 - }, 97 - { headers: JSON_HEADERS }, 98 - ); 92 + } ); 99 93 } 100 94 101 95 // GET /api/status/service/:id ··· 104 98 const uptime = await getUptime7d(env.DB, id); 105 99 106 100 if (!ping) { 107 - return Response.json({ error: "service not found" }, { status: 404, headers: JSON_HEADERS }); 101 + return Response.json({ error: "service not found" }, { status: 404 }); 108 102 } 109 103 110 104 return Response.json( ··· 113 107 status: ping.status, 114 108 latency_ms: ping.latency_ms, 115 109 uptime_7d: uptime, 116 - }, 117 - { headers: JSON_HEADERS }, 118 - ); 110 + } ); 119 111 } 120 112 121 113 // GET /api/status/machine/:name ··· 124 116 const machine = manifest[name]; 125 117 126 118 if (!machine) { 127 - return Response.json({ error: "machine not found" }, { status: 404, headers: JSON_HEADERS }); 119 + return Response.json({ error: "machine not found" }, { status: 404 }); 128 120 } 129 121 130 122 const online = await getDeviceStatus(env, machine.tailscale_host); ··· 152 144 online, 153 145 status, 154 146 services, 155 - }, 156 - { headers: JSON_HEADERS }, 157 - ); 147 + } ); 158 148 } 159 149 160 150 export async function handleStatusRoute(
-1
src/routes/uptime.ts
··· 13 13 14 14 return Response.json( 15 15 { service_id: serviceId, window_hours, buckets } satisfies UptimeResponse, 16 - { headers: { "Access-Control-Allow-Origin": "*" } }, 17 16 ); 18 17 }
+138
src/schemas.ts
··· 1 + const serviceStatusEnum = { 2 + type: "string", 3 + enum: ["up", "degraded", "misconfigured", "timeout", "down", "unknown"], 4 + } as const; 5 + 6 + const machineStatusEnum = { 7 + type: "string", 8 + enum: ["up", "degraded", "partial", "down", "unknown"], 9 + } as const; 10 + 11 + const service = { 12 + type: "object", 13 + properties: { 14 + id: { type: "string" }, 15 + status: serviceStatusEnum, 16 + latency_ms: { type: ["number", "null"] }, 17 + uptime_7d: { type: "number" }, 18 + }, 19 + required: ["id", "status", "latency_ms", "uptime_7d"], 20 + additionalProperties: false, 21 + } as const; 22 + 23 + const machine = { 24 + type: "object", 25 + properties: { 26 + name: { type: "string" }, 27 + hostname: { type: "string" }, 28 + type: { type: "string" }, 29 + online: { type: "boolean" }, 30 + status: machineStatusEnum, 31 + services: { type: "array", items: service }, 32 + }, 33 + required: ["name", "hostname", "type", "online", "status", "services"], 34 + additionalProperties: false, 35 + } as const; 36 + 37 + export const schemas: Record<string, object> = { 38 + status: { 39 + $schema: "http://json-schema.org/draft-07/schema#", 40 + title: "Status", 41 + description: "GET /api/status", 42 + type: "object", 43 + properties: { 44 + ok: { type: "boolean" }, 45 + status: machineStatusEnum, 46 + machines: { type: "array", items: machine }, 47 + }, 48 + required: ["ok", "status", "machines"], 49 + additionalProperties: false, 50 + $defs: { service, machine, serviceStatus: serviceStatusEnum, machineStatus: machineStatusEnum }, 51 + }, 52 + 53 + "status-overall": { 54 + $schema: "http://json-schema.org/draft-07/schema#", 55 + title: "StatusOverall", 56 + description: "GET /api/status/overall", 57 + type: "object", 58 + properties: { 59 + ok: { type: "boolean" }, 60 + status: machineStatusEnum, 61 + uptime_7d: { type: "number" }, 62 + services_total: { type: "integer" }, 63 + services_monitored: { type: "integer" }, 64 + machines_total: { type: "integer" }, 65 + }, 66 + required: ["ok", "status", "uptime_7d", "services_total", "services_monitored", "machines_total"], 67 + additionalProperties: false, 68 + }, 69 + 70 + "status-service": { 71 + $schema: "http://json-schema.org/draft-07/schema#", 72 + title: "StatusService", 73 + description: "GET /api/status/service/:id", 74 + type: "object", 75 + properties: { 76 + id: { type: "string" }, 77 + status: serviceStatusEnum, 78 + latency_ms: { type: ["number", "null"] }, 79 + uptime_7d: { type: "number" }, 80 + }, 81 + required: ["id", "status", "latency_ms", "uptime_7d"], 82 + additionalProperties: false, 83 + }, 84 + 85 + "status-machine": { 86 + $schema: "http://json-schema.org/draft-07/schema#", 87 + title: "StatusMachine", 88 + description: "GET /api/status/machine/:name", 89 + type: "object", 90 + properties: { 91 + name: { type: "string" }, 92 + hostname: { type: "string" }, 93 + type: { type: "string" }, 94 + online: { type: "boolean" }, 95 + status: machineStatusEnum, 96 + services: { type: "array", items: service }, 97 + }, 98 + required: ["name", "hostname", "type", "online", "status", "services"], 99 + additionalProperties: false, 100 + }, 101 + 102 + uptime: { 103 + $schema: "http://json-schema.org/draft-07/schema#", 104 + title: "Uptime", 105 + description: "GET /api/uptime/:service_id", 106 + type: "object", 107 + properties: { 108 + service_id: { type: "string" }, 109 + window_hours: { type: "integer" }, 110 + buckets: { 111 + type: "array", 112 + items: { 113 + type: "object", 114 + properties: { 115 + timestamp: { type: "string", format: "date-time" }, 116 + status: serviceStatusEnum, 117 + }, 118 + required: ["timestamp", "status"], 119 + additionalProperties: false, 120 + }, 121 + }, 122 + }, 123 + required: ["service_id", "window_hours", "buckets"], 124 + additionalProperties: false, 125 + }, 126 + 127 + error: { 128 + $schema: "http://json-schema.org/draft-07/schema#", 129 + title: "Error", 130 + description: "Error response (e.g. 404)", 131 + type: "object", 132 + properties: { 133 + error: { type: "string" }, 134 + }, 135 + required: ["error"], 136 + additionalProperties: false, 137 + }, 138 + };