Skip to content

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.org login is an admin
  • [email protected] — 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/backfill with { "catalog_id": "<uuid>" }202 { job_id }. Poll GET /api/admin/onboarding-jobs/:id.
  • Runs the same pipeline as onboarding (colleges → courses/prereqs → programs incl. the flat-catalog rescue → gen-ed) via runYearBackfill in onboarding-orchestrator.ts, skipping search/fingerprint/year-enumeration.
  • Invariant: a back-fill never touches institutions.onboarding_state and 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 carry onboarding_jobs.kind = 'backfill' (migration 20260613_168), and the startup stranded-job sweep routes them to job-row-only failure semantics instead of demoting the institution (#1094).
  • Returns 409 if 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_id is set null; the response reports how many maps were detached this way.
  • Refused with 409 while 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) and GET /api/admin/stats/costs (cost_ledger aggregates for operation_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: true and 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) or upper_division (migration 20260612_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 to four_year WILL 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=…), or PATCH /api/admin/catalogs/institutions/:id with {"admission_model": "..."}. The response includes stale_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/AdmissionModel in packages/course-map-shared — the zod enum, worker read paths, and frontend all derive from it.