mirror of
https://github.com/prdlk/leetcode.git
synced 2026-09-17 15:36:26 +00:00
88 lines
3.0 KiB
TypeScript
88 lines
3.0 KiB
TypeScript
/**
|
|||
|
|
* 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<Stage, number>;
|
||
|
|
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<string, unknown>;
|
||
|
|
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<Stats> {
|
||
|
|
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;
|
||
|
|
}
|