Dashboard Create-Map Wizard & User School Imports (#1073)
Sign-in is open to anyone. After sign-in, users land on /dashboard; staff
(@theaccessible.org emails) are bounced to /admin by
apps/course-map/src/components/StaffRedirect.tsx. The old school-email
onboarding gate (OnboardingGate.tsx) is gone β the workerβs
/api/onboarding/me domain-funnel endpoint still exists but the frontend no
longer calls it.
Wizard flow
apps/course-map/src/components/CreateMapWizard.tsx renders at the top of the
dashboard: School β Catalog year β College β Department β Degree cascading
selects, then Create course map generates a fresh map the user owns and
navigates to /map?id=β¦. Generation takes ~30β60 s; a role="status"
progress note shows while it runs.
Future: when the advisor review workflow lands, the create endpoint should prefer an advisor-reviewed map for the program instead of always generating fresh. The service split below was made with that swap in mind.
Worker endpoints (/api/catalog/*, requireUser, NOT admin)
workers/course-map-api/src/routes/catalog-browse.ts:
| Endpoint | Purpose |
|---|---|
GET /api/catalog/schools | Institutions with β₯1 catalog year that has programs (camelCase) |
GET /api/catalog/catalogs/:id/tree | College β department β degree cascade, empty branches pruned |
POST /api/catalog/programs/:id/generate-map | Generate + persist a map owned by the caller |
POST /api/catalog/import-requests | βMy school is not in the listβ β starts the onboarding orchestrator |
GET /api/catalog/import-requests/active | The callerβs in-flight import, if any |
Shared services extracted so admin and user routes donβt duplicate logic:
services/catalog-picker.tsβbuildPickerTree(catalogId)(also feeds the admin/picker-treeendpoint)services/map-from-program.tsβcreateCourseMapFromProgram(programId, userId)(also feeds adminPOST /api/admin/programs/:id/generate-map)
Import-request guardrails
- Dedupe: if the schoolβs institution row is
readyβ 409already_available; if a matching job is already queued/running (by institution link orinput_nameilike) β the requester is attached as a watcher instead of starting a duplicate crawl (202watching). - One active import per user: a second request while one is running β 409
import_in_flight(repeating the same school is an idempotent 202). - Global ceiling: max 5 concurrent onboarding jobs β 429 with
Retry-After: 600. Count failures fail closed.
Watchers & ready email
Migration 20260611_170_onboarding_watchers.sql adds onboarding_watchers
(job_id, user_id) β the many-to-many βemail these users when the job
succeedsβ set (failed jobs notify ops only, via notifyOpsNeedsManual).
services/onboarding-email.ts#notifyUserReady unions watchers
with the legacy notify_user_id/notify_email_when_done opt-in pair, resolves
emails through the Supabase auth admin API, dedupes, and sends one Resend
email per recipient (watchers are strangers β never share a To: line).
PDF upload
The upload flow stays as the secondary path: a bordered βUpload a flow chart insteadβ link next to the βYour course mapsβ list heading.