Course Map Admin Operations
Operator tooling added in #1084, all under map.theaccessible.org/admin. Every
endpoint sits behind requireUser + requireAdmin.
Who is an admin
Admin access is resolved server-side in workers/course-map-api/src/auth.ts
from two env vars on the worker (/home/larry/accessible/.env.course-map-api
on 10.1.1.4):
ADMIN_EMAIL_DOMAINS=theaccessible.org— any@theaccessible.orglogin is an adminADMIN_EMAILS=larry@anglin.com— exact-email allowlist for accounts outside that domain
There is no client-side gate; non-admins get a 403 and the pages render a “not authorized” message.
Back-fill a prior catalog year
Onboarding fully ingests only the newest catalog year; the other four
discovered years are registered in institution_catalogs but left empty.
- UI: Admin → Catalogs list → View a year → Operations → “Run full ingest for this year”. The page polls the job and shows the last few log lines; it’s safe to navigate away — the job runs server-side.
- API:
POST /api/admin/onboarding-jobs/backfillwith{ "catalog_id": "<uuid>" }→202 { job_id }. PollGET /api/admin/onboarding-jobs/:id. - Runs the same pipeline as onboarding (colleges → courses/prereqs → programs
incl. the flat-catalog rescue → gen-ed) via
runYearBackfillinonboarding-orchestrator.ts, skipping search/fingerprint/year-enumeration. - Invariant: a back-fill never touches
institutions.onboarding_stateand never sends the needs_manual/ready emails — the school is already live on its newest year. Failures only mark the job row failed. This holds on every path, including a process restart: back-fill jobs carryonboarding_jobs.kind = 'backfill'(migration20260613_168), and the startup stranded-job sweep routes them to job-row-only failure semantics instead of demoting the institution (#1094). - Returns
409if any onboarding/back-fill job is queued or running for the same institution. - Idempotent: re-running on an already-ingested year upserts in place.
- Expect the same politeness-limited crawl time as the original onboard (minutes to ~an hour per year).
Delete a catalog year
- UI: same Operations section → “Delete this catalog…” with an inline confirm step.
- API:
DELETE /api/admin/catalogs/catalogs/:id. - DB cascades remove courses, prereqs, colleges, departments, programs,
requirements, and ingestion jobs for that year. User course maps survive
— their
source_program_idis set null; the response reports how many maps were detached this way. - Refused with
409while a pipeline job is running for the institution.
All course maps
- UI: Admin → “All course maps” (
/admin/maps). Searchable (name or school), paginated, shows owner email, extraction/review status, and upload-vs-generated source. - API:
GET /api/admin/maps?page=1&pageSize=25&q=.... Owner emails are resolved per-page via the Supabase auth admin API (emails are deliberately not stored on app tables).
Usage & cost stats
- UI: Admin → “Usage & costs” (
/admin/stats). - API:
GET /api/admin/stats(maps totals, last-30-days, per-day curve, distinct users, share views, registry sizes) andGET /api/admin/stats/costs(cost_ledger aggregates foroperation_type = 'course-map-extraction': total, by kind, by month, recent 20 operations). - Aggregation happens in the worker (PostgREST has no GROUP BY). Fetches are
capped at 10,000 rows; if the cap is hit the response carries
truncated: trueand the UI flags totals as a floor.
Upper-division institutions (#1087)
Some schools (e.g. Texas A&M–Central Texas) admit juniors/seniors only, but their catalogs list lower-division courses (transfer articulation) and four-year degree plans — generated maps would imply all four years happen there.
institutions.admission_model:four_year(default) orupper_division(migration20260612_167).- Auto-detection: during college discovery the pipeline phrase-scans the
catalog root (“upper-level university”, “offers only junior- and
senior-level courses”) and flips the flag one-way to
upper_division, logging it in the job. It never auto-downgrades — but note a manual downgrade tofour_yearWILL be re-flipped on the next ingest if the catalog still carries the phrase. - Manual override: the Operations section of the catalog detail page
(
/admin/catalog?id=…), orPATCH /api/admin/catalogs/institutions/:idwith{"admission_model": "..."}. The response includesstale_label_maps— how many of the school’s existing generated maps keep their old semester labels until regenerated — and the UI shows it after a change (#1094). - Effect: newly generated maps label semesters 1–4 “Year N Fall/Spring — before transfer” (labels flow into print and exports), and the map + share views show an explanatory banner. Existing maps pick up the banner immediately (it’s resolved at read time) but keep their old semester labels until regenerated. The banner copy deliberately doesn’t reference the “before transfer” labels, so it can’t contradict a map generated before the flag flipped.
- The two-value domain has a single source of truth:
ADMISSION_MODELS/AdmissionModelinpackages/course-map-shared— the zod enum, worker read paths, and frontend all derive from it.