export interface Env { ASSETS: Fetcher; DB: D1Database; OVERLAY_CACHE: KVNamespace; ADMIN_TOKEN: string; } interface OverlayEntry { status?: string | null; broken?: { reason: string; since?: string } | null; tags?: string[]; } // Short TTL, not a long-lived cache - the whole point of this overlay is that a status/broken/tag // flip from a phone should be visible almost immediately, not after a long edge-cache window. const CACHE_KEY = "overlay:v1"; const CACHE_TTL_SECONDS = 60; // Workers KV's enforced minimum const CORS_HEADERS = { "access-control-allow-origin": "*", "access-control-allow-methods": "GET,PUT,DELETE,OPTIONS", "access-control-allow-headers": "authorization,content-type", }; function json(data: unknown, status = 200): Response { return new Response(JSON.stringify(data), { status, headers: { "content-type": "application/json", ...CORS_HEADERS }, }); } async function readAll(db: D1Database): Promise> { const { results } = await db.prepare("SELECT * FROM overlay").all>(); const out: Record = {}; for (const row of results) { const id = row.id as string; out[id] = { status: (row.status as string) ?? undefined, broken: row.broken_reason ? { reason: row.broken_reason as string, since: row.broken_since as string } : null, tags: row.tags ? JSON.parse(row.tags as string) : [], }; } return out; } export default { async fetch(request: Request, env: Env): Promise { try { return await handle(request, env); } catch (e) { console.error(e); return json({ error: "internal" }, 500); } }, }; async function handle(request: Request, env: Env): Promise { const url = new URL(request.url); if (request.method === "OPTIONS" && url.pathname.startsWith("/api/")) { return new Response(null, { headers: CORS_HEADERS }); } if (!url.pathname.startsWith("/api/")) { return env.ASSETS.fetch(request); } // GET /api/overlay - public read, used at site build time (generate-site.mjs) and by the // admin plugin/site to show current values before editing. if (url.pathname === "/api/overlay" && request.method === "GET") { const cached = await env.OVERLAY_CACHE.get(CACHE_KEY, "json"); if (cached) return json(cached); const data = await readAll(env.DB); await env.OVERLAY_CACHE.put(CACHE_KEY, JSON.stringify(data), { expirationTtl: CACHE_TTL_SECONDS }); return json(data); } const match = url.pathname.match(/^\/api\/overlay\/([\w-]+)$/); if (match) { const id = match[1]; const auth = request.headers.get("authorization"); if (auth !== `Bearer ${env.ADMIN_TOKEN}`) { return json({ error: "unauthorized" }, 401); } if (request.method === "PUT") { const body = await request.json().catch(() => null); if (!body) return json({ error: "invalid body" }, 400); await env.DB.prepare( `INSERT INTO overlay (id, status, broken_reason, broken_since, tags, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6) ON CONFLICT(id) DO UPDATE SET status = excluded.status, broken_reason = excluded.broken_reason, broken_since = excluded.broken_since, tags = excluded.tags, updated_at = excluded.updated_at`, ).bind( id, body.status ?? null, body.broken?.reason ?? null, body.broken ? (body.broken.since ?? new Date().toISOString()) : null, JSON.stringify(body.tags ?? []), new Date().toISOString(), ).run(); await env.OVERLAY_CACHE.delete(CACHE_KEY); return json({ ok: true }); } if (request.method === "DELETE") { await env.DB.prepare("DELETE FROM overlay WHERE id = ?1").bind(id).run(); await env.OVERLAY_CACHE.delete(CACHE_KEY); return json({ ok: true }); } } return json({ error: "not found" }, 404); }