Skip to content

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:

EndpointPurpose
GET /api/catalog/schoolsInstitutions with β‰₯1 catalog year that has programs (camelCase)
GET /api/catalog/catalogs/:id/treeCollege β†’ department β†’ degree cascade, empty branches pruned
POST /api/catalog/programs/:id/generate-mapGenerate + 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/activeThe 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-tree endpoint)
  • services/map-from-program.ts β€” createCourseMapFromProgram(programId, userId) (also feeds admin POST /api/admin/programs/:id/generate-map)

Import-request guardrails

  • Dedupe: if the school’s institution row is ready β†’ 409 already_available; if a matching job is already queued/running (by institution link or input_name ilike) β†’ the requester is attached as a watcher instead of starting a duplicate crawl (202 watching).
  • 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.