diff --git a/src/index.ts b/src/index.ts index 42d8d4c..98f8ab6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,40 +8,71 @@ import { handleFavicon } from "./routes/favicon"; import { handleUptime } from "./routes/uptime"; import { handleBadgeRoute } from "./routes/badge"; import { handleIndex } from "./routes/index"; +import { schemas } from "./schemas"; -export default { - async fetch(request: Request, env: Env): Promise { - const url = new URL(request.url); - const path = url.pathname; +async function handleRequest(request: Request, env: Env): Promise { + const url = new URL(request.url); + const path = url.pathname; - if (path === "/" || path === "") { - return handleIndex(env); - } + if (path === "/" || path === "") { + return handleIndex(env); + } - if (path === "/favicon.svg") { - return handleFavicon(env); - } + if (path === "/favicon.svg") { + return handleFavicon(env); + } - if (path === "/health") { - return Response.json({ ok: true, timestamp: new Date().toISOString() }); - } + if (path === "/health") { + return Response.json({ ok: true, timestamp: new Date().toISOString() }); + } - if (path.startsWith("/api/status")) { - const res = await handleStatusRoute(env, path); - if (res) return res; - } + if (path === "/api/schemas") { + return Response.json(schemas); + } - const uptimeMatch = path.match(/^\/api\/uptime\/(.+)$/); - if (uptimeMatch) { - return handleUptime(env, uptimeMatch[1], url); + const schemaMatch = path.match(/^\/api\/schemas\/(.+)$/); + if (schemaMatch) { + const schema = schemas[schemaMatch[1]]; + if (schema) { + return Response.json(schema); } + return Response.json({ error: "schema not found" }, { status: 404 }); + } - if (path.startsWith("/badge")) { - const badge = await handleBadgeRoute(env, path, url); - if (badge) return badge; + if (path.startsWith("/api/status")) { + const res = await handleStatusRoute(env, path); + if (res) return res; + } + + const uptimeMatch = path.match(/^\/api\/uptime\/(.+)$/); + if (uptimeMatch) { + return handleUptime(env, uptimeMatch[1], url); + } + + if (path.startsWith("/badge")) { + const badge = await handleBadgeRoute(env, path, url); + if (badge) return badge; + } + + return new Response("Not Found", { status: 404 }); +} + +export default { + async fetch(request: Request, env: Env): Promise { + if (request.method === "OPTIONS") { + return new Response(null, { + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", + }, + }); } - return new Response("Not Found", { status: 404 }); + const response = await handleRequest(request, env); + const corsResponse = new Response(response.body, response); + corsResponse.headers.set("Access-Control-Allow-Origin", "*"); + return corsResponse; }, async scheduled(_controller: ScheduledController, env: Env): Promise { diff --git a/src/routes/status.ts b/src/routes/status.ts index 5976420..6228087 100644 --- a/src/routes/status.ts +++ b/src/routes/status.ts @@ -3,8 +3,6 @@ import { getManifest } from "../manifest"; import { getLatestPing, getUptime7d } from "../db"; import { getDeviceStatus } from "../tailscale"; -const JSON_HEADERS = { "Access-Control-Allow-Origin": "*" }; - function worstStatus(statuses: string[]): string { if (statuses.length === 0) return "unknown"; if (statuses.every((s) => s === "down" || s === "timeout")) return "down"; @@ -45,9 +43,7 @@ async function overallStatus(env: Env): Promise { services_total: allServices.length, services_monitored: monitored.length, machines_total: Object.keys(manifest).length, - }, - { headers: JSON_HEADERS }, - ); + } ); } // GET /api/status @@ -93,9 +89,7 @@ async function fullStatus(env: Env): Promise { ok: status === "up", status, machines, - }, - { headers: JSON_HEADERS }, - ); + } ); } // GET /api/status/service/:id @@ -104,7 +98,7 @@ async function serviceStatus(env: Env, id: string): Promise { const uptime = await getUptime7d(env.DB, id); if (!ping) { - return Response.json({ error: "service not found" }, { status: 404, headers: JSON_HEADERS }); + return Response.json({ error: "service not found" }, { status: 404 }); } return Response.json( @@ -113,9 +107,7 @@ async function serviceStatus(env: Env, id: string): Promise { status: ping.status, latency_ms: ping.latency_ms, uptime_7d: uptime, - }, - { headers: JSON_HEADERS }, - ); + } ); } // GET /api/status/machine/:name @@ -124,7 +116,7 @@ async function machineStatus(env: Env, name: string): Promise { const machine = manifest[name]; if (!machine) { - return Response.json({ error: "machine not found" }, { status: 404, headers: JSON_HEADERS }); + return Response.json({ error: "machine not found" }, { status: 404 }); } const online = await getDeviceStatus(env, machine.tailscale_host); @@ -152,9 +144,7 @@ async function machineStatus(env: Env, name: string): Promise { online, status, services, - }, - { headers: JSON_HEADERS }, - ); + } ); } export async function handleStatusRoute( diff --git a/src/routes/uptime.ts b/src/routes/uptime.ts index b1c4a58..ca57603 100644 --- a/src/routes/uptime.ts +++ b/src/routes/uptime.ts @@ -13,6 +13,5 @@ export async function handleUptime( return Response.json( { service_id: serviceId, window_hours, buckets } satisfies UptimeResponse, - { headers: { "Access-Control-Allow-Origin": "*" } }, ); } diff --git a/src/schemas.ts b/src/schemas.ts new file mode 100644 index 0000000..1ca5935 --- /dev/null +++ b/src/schemas.ts @@ -0,0 +1,138 @@ +const serviceStatusEnum = { + type: "string", + enum: ["up", "degraded", "misconfigured", "timeout", "down", "unknown"], +} as const; + +const machineStatusEnum = { + type: "string", + enum: ["up", "degraded", "partial", "down", "unknown"], +} as const; + +const service = { + type: "object", + properties: { + id: { type: "string" }, + status: serviceStatusEnum, + latency_ms: { type: ["number", "null"] }, + uptime_7d: { type: "number" }, + }, + required: ["id", "status", "latency_ms", "uptime_7d"], + additionalProperties: false, +} as const; + +const machine = { + type: "object", + properties: { + name: { type: "string" }, + hostname: { type: "string" }, + type: { type: "string" }, + online: { type: "boolean" }, + status: machineStatusEnum, + services: { type: "array", items: service }, + }, + required: ["name", "hostname", "type", "online", "status", "services"], + additionalProperties: false, +} as const; + +export const schemas: Record = { + status: { + $schema: "http://json-schema.org/draft-07/schema#", + title: "Status", + description: "GET /api/status", + type: "object", + properties: { + ok: { type: "boolean" }, + status: machineStatusEnum, + machines: { type: "array", items: machine }, + }, + required: ["ok", "status", "machines"], + additionalProperties: false, + $defs: { service, machine, serviceStatus: serviceStatusEnum, machineStatus: machineStatusEnum }, + }, + + "status-overall": { + $schema: "http://json-schema.org/draft-07/schema#", + title: "StatusOverall", + description: "GET /api/status/overall", + type: "object", + properties: { + ok: { type: "boolean" }, + status: machineStatusEnum, + uptime_7d: { type: "number" }, + services_total: { type: "integer" }, + services_monitored: { type: "integer" }, + machines_total: { type: "integer" }, + }, + required: ["ok", "status", "uptime_7d", "services_total", "services_monitored", "machines_total"], + additionalProperties: false, + }, + + "status-service": { + $schema: "http://json-schema.org/draft-07/schema#", + title: "StatusService", + description: "GET /api/status/service/:id", + type: "object", + properties: { + id: { type: "string" }, + status: serviceStatusEnum, + latency_ms: { type: ["number", "null"] }, + uptime_7d: { type: "number" }, + }, + required: ["id", "status", "latency_ms", "uptime_7d"], + additionalProperties: false, + }, + + "status-machine": { + $schema: "http://json-schema.org/draft-07/schema#", + title: "StatusMachine", + description: "GET /api/status/machine/:name", + type: "object", + properties: { + name: { type: "string" }, + hostname: { type: "string" }, + type: { type: "string" }, + online: { type: "boolean" }, + status: machineStatusEnum, + services: { type: "array", items: service }, + }, + required: ["name", "hostname", "type", "online", "status", "services"], + additionalProperties: false, + }, + + uptime: { + $schema: "http://json-schema.org/draft-07/schema#", + title: "Uptime", + description: "GET /api/uptime/:service_id", + type: "object", + properties: { + service_id: { type: "string" }, + window_hours: { type: "integer" }, + buckets: { + type: "array", + items: { + type: "object", + properties: { + timestamp: { type: "string", format: "date-time" }, + status: serviceStatusEnum, + }, + required: ["timestamp", "status"], + additionalProperties: false, + }, + }, + }, + required: ["service_id", "window_hours", "buckets"], + additionalProperties: false, + }, + + error: { + $schema: "http://json-schema.org/draft-07/schema#", + title: "Error", + description: "Error response (e.g. 404)", + type: "object", + properties: { + error: { type: "string" }, + }, + required: ["error"], + additionalProperties: false, + }, +};