Files
leetcode/api

srs-api

Cloudflare Worker running the spaced-repetition system: daily digest email, one-tap logging, /done webhook, Saturday review issues, live SVG charts, and the /api/stats feed for the docs progress page.

Live at https://srs-api.prdlk.workers.dev.

Direction of truth

  • D1 owns SRS state — stages, review dates, attempt history, gate scores, boost flags. Nothing else is authoritative.
  • GitHub issues own the catalog — topics, problems, set:*/diff:* labels, milestones. catalog.ts reconciles them into D1 nightly (and via /admin/reconcile); it recomputes from scratch, never invents rows, and never overwrites SRS-owned columns (stage, next_review).
  • The repo owns the scheduledata/schedule.json, human-edited, bundled at deploy. Git history is its audit log.
  • The GitHub Project mirror (Target Date, SRS Stage, First Attempt) is best-effort: failures log warnings, never block a D1 write. Topic rows are never written.

The ladder

new → +2 → +5 → +10 → retired; pass advances, fail resets to +2. A problem's first-ever log enters at +2 regardless of result. Stage names the NEXT review's interval. All dates are ET calendar strings anchored at noon UTC (src/srs.ts) — never raw Date math.

Blind drills draw only from topics already learned: scheduled in an earlier week (the current week's optional pool is reserved for Saturday's gate) and with at least one core problem on the ladder — a skipped learning day never feeds drills just because its calendar week lapsed.

Routes

Route Auth Purpose
GET /log?p&r&d&sig HMAC (LINK_KEY) one-tap pass/fail; idempotent per (problem, date, kind); links expire after 3 days
POST /webhook/github HMAC (WEBHOOK_SECRET) /done <n> pass|fail comments (owner only, any issue); review-issue close → gate scoring
GET /chart/{progress,ladder,heatmap}.svg, GET /badge/gate.svg public hand-rolled SVGs, max-age=300 (GitHub Camo's freshness floor)
GET /api/stats public, CORS-pinned to the docs origin one JSON document for /progress
POST /admin/{digest,review,reconcile} Authorization: Bearer <LINK_KEY> manual triggers; digest takes ?dry=1&force=1&date=

Crons (DST-proof)

Each event has two UTC crons; code fires only when the computed ET hour matches (etHour in src/srs.ts, unit-checked in src/srs.test.ts):

  • 0 12,13 * * * → 8 AM ET: catalog reconcile, then the digest.
  • 0 4,5 * * 6 → midnight ET Saturday: create Review — Week N (so the 8 AM digest can link to it).

Secrets & bindings

wrangler secret putGH_PAT (Issues + Projects RW), WEBHOOK_SECRET (matches the repo webhook), LINK_KEY (signs one-tap links, gates admin routes). Bindings in wrangler.jsonc: DB (D1 srs), EMAIL (send_email, restricted to the verified destination). Sender domain prdlk.com is onboarded to Email Sending.

Local dev

cp .dev.vars.example .dev.vars       # or fill GH_PAT/WEBHOOK_SECRET/LINK_KEY
bun install
bunx wrangler d1 migrations apply srs --local
bun run dev                          # wrangler dev on :8787, local D1
curl -X POST -H "Authorization: Bearer $LINK_KEY" \
  "localhost:8787/admin/digest?dry=1&date=2026-08-31"   # prints HTML, sends nothing
bun test src                         # DST guard + date math

?date= on admin routes is the SRS_TODAY equivalent. The one-shot migration from the retired .github/srs/srs.json lives at scripts/import-srs.ts (--remote for production D1); it is re-runnable — import-sourced attempts are wiped and re-inserted.

Deploy

bun run deploy        # from api/, or `bun run api:deploy` at the repo root

Manual by design — never coupled to the docs deploy. After changing bindings, rerun bunx wrangler types.