mirror of
https://github.com/prdlk/leetcode.git
synced 2026-09-16 23:16:26 +00:00
127 lines
6.8 KiB
Markdown
127 lines
6.8 KiB
Markdown
# 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 schedule** — `data/schedule.json`, human-edited,
|
||
bundled at deploy. Git history is its audit log.
|
||
- The GitHub Project mirror writes `Target Date` and nothing else: it is
|
||
best-effort, so failures log warnings and never block a D1 write, and topic
|
||
rows are never written. SRS stage and first-attempt result stay in D1 only —
|
||
the charts, digest and `/api/stats` read them from there, so mirroring them
|
||
into Project single-selects bought only drift. Do not re-add them.
|
||
`Set`/`Difficulty` are a projection of the issue labels, reconciled by
|
||
`bun run sync-project-fields` at the repo root, not by this Worker.
|
||
|
||
## The ladder
|
||
|
||
`new → +3 → +7 → retired`; pass advances, fail resets to `+3`. A problem's
|
||
first-ever log enters at `+3` regardless of result. Stage names the NEXT
|
||
review's interval, and the two rungs are the same numbers as the `work/3` and
|
||
`work/7` buckets `bun run pick` scaffolds re-solves into.
|
||
|
||
**Sunday is never booked.** Topics run Mon–Fri, the gate is Saturday, and 3/7
|
||
are chosen so a solve returns on a working day: only a Thursday solve's `+3`
|
||
would land on the rest day, and `workingDay()` in `src/srs.ts` slides it to
|
||
Monday. Every scheduled date in the Worker — ladder reviews, levelled
|
||
overflow, deferred-Hard release dates — is minted by that one function, so the
|
||
rest day cannot be booked and then swallowed by the digest's Sunday branch.
|
||
Reviews may slip later, never earlier: pulling one back to Saturday would
|
||
shorten the interval it exists to test. `src/srs.test.ts` sweeps the whole
|
||
campaign calendar to prove it.
|
||
|
||
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/solved` | `Authorization: Bearer <LINK_KEY>` | `{"solved":[{"lc":1,"bucket":1},…]}` — solutions committed under `work/`; the bucket names the rung it settles (1 → `new`, 3 → `+3`, 7 → `+7`), so a first solve enters the ladder and a pushed re-solve advances it. Writes only when the problem stands on that rung, which is what makes re-posting the whole set a no-op. Takes `?dry=1&date=` |
|
||
| `POST /admin/due` | `Authorization: Bearer <LINK_KEY>` | read-only: what the ladder has due on `?date=` (default today), oldest first, with the rung/bucket, how late it is, and when it was last seen. The one answer behind the daily `Spaced Repetition` issue |
|
||
| `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 put` — `GH_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.
|
||
|
||
Three Actions workflows talk to this Worker with the `SRS_ADMIN_KEY` repo
|
||
secret:
|
||
|
||
- `sync-d1.yml` sends curriculum issue edits to `POST /admin/reconcile` (the
|
||
morning cron is the backstop).
|
||
- `close-solved.yml` sends the whole implemented `work/` set — each entry
|
||
tagged with its bucket — to `POST /admin/solved` on every push to `main`.
|
||
That call is what keeps a committed solution from being invisible here:
|
||
closing an issue is not a state change the Worker can see (the catalog
|
||
reconcile ignores issue state, and only `logAttempt()` moves the ladder), and
|
||
the bucket is what lets a pushed re-solve advance `+3 → +7` instead of
|
||
being read as a stale first solve. `close-solved` still owns the issue close
|
||
itself (it knows the files and the commit), so this path only writes D1 and
|
||
mirrors Project fields.
|
||
- `spaced-repetition.yml` reads `POST /admin/due` at 06:00 ET and renders
|
||
today's `Spaced Repetition` issue. Read-only, and the only schedule involved
|
||
is this Worker's ladder.
|
||
|
||
## Local dev
|
||
|
||
```sh
|
||
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 guards, date math, the rest-day sweep
|
||
```
|
||
|
||
`?date=` on admin routes is the `SRS_TODAY` equivalent. Migrations are
|
||
append-only and applied in order; `0002_ladder_3_7.sql` is the +2/+5/+10 →
|
||
`+3`/`+7` rebuild, which also lifts every date that was sitting on a Sunday.
|
||
The one-shot importer for the retired `.github/srs/srs.json` is gone — that
|
||
state no longer exists, and git history is its record.
|
||
|
||
## Deploy
|
||
|
||
```sh
|
||
bun run deploy # from apps/api/, or `bun run api:deploy` at the repo root
|
||
```
|
||
|
||
CI deploys on every push to `main` (`deploy.yml`, gated on `bun run api:test`,
|
||
using the `CLOUDFLARE_API_TOKEN`/`CLOUDFLARE_ACCOUNT_ID` repo secrets); the
|
||
commands above are for out-of-band deploys. After changing bindings, rerun
|
||
`bunx wrangler types`.
|