/** * The SRS Worker, from the CLI side: one base URL and one read-side payload. * * `SRS_API` points a script at a `wrangler dev` instance instead of production * — the same switch close-solved.ts uses for its `/admin/solved` push, which is * why the default lives here rather than in either script. * * The payload is narrowed with a guard rather than cast: D1 is the only source * of SRS truth, so if the Worker answers something else the caller must say so * out loud, not render half a dashboard off undefined fields. */ import { STAGES, TEMPERATURES, type Stage, type Temperature } from "../api/src/srs.ts"; export const SRS_API = process.env.SRS_API ?? "https://srs-api.prdlk.workers.dev"; // The rungs and the bands come straight from the Worker's domain module — the // same import work.ts makes for WINDOWS — so a change to either vocabulary // reaches this dashboard without a second list to remember. export { STAGES, TEMPERATURES, type Stage, type Temperature }; export interface Phase { milestone: number; name: string; core_done: number; core_total: number; optional_done: number; optional_total: number; deferred_done: number; deferred_total: number; } export interface Topic { issue: number; name: string; milestone: number | null; week: number | null; taught: boolean; total: number; done: number; retired: number; due: number; last: string | null; age: number | null; temperature: Temperature; } export interface Stats { generated: string; campaign: { day: number; week: number; start: string; days: number }; phases: Phase[]; ladder: Record; difficulty: { difficulty: string; total: number; done: number }[]; topics: Topic[]; /** week is the 1-based campaign week — the Worker converts from ISO. */ gates: { week: number; issue: number | null; pass_rate: number | null; closed_on: string | null }[]; streak: number; queue: { date: string; due: number }[]; /** Every campaign day, zeros and future days included. */ heat: { date: string; attempts: number; passes: number }[]; } function isStats(value: unknown): value is Stats { if (typeof value !== "object" || value === null) return false; const v = value as Record; return ( typeof v.campaign === "object" && v.campaign !== null && typeof v.ladder === "object" && v.ladder !== null && typeof v.streak === "number" && Array.isArray(v.phases) && Array.isArray(v.difficulty) && Array.isArray(v.topics) && Array.isArray(v.gates) && Array.isArray(v.queue) && Array.isArray(v.heat) ); } /** GET /api/stats. Throws with the reason; there is no local substitute. */ export async function fetchStats(signal?: AbortSignal): Promise { const res = await fetch(`${SRS_API}/api/stats`, { signal }); if (!res.ok) throw new Error(`GET /api/stats -> ${res.status} ${await res.text()}`); const body: unknown = await res.json(); if (!isStats(body)) throw new Error("GET /api/stats -> unrecognised payload shape"); return body; }