Skip to content

feat: multi-tenancy, auth, superadmin dashboard, onboarding, and test suite#1

Merged
guangshinhaha merged 18 commits into
mainfrom
claude/reliefcher-new-feature-IViMe
Apr 2, 2026
Merged

feat: multi-tenancy, auth, superadmin dashboard, onboarding, and test suite#1
guangshinhaha merged 18 commits into
mainfrom
claude/reliefcher-new-feature-IViMe

Conversation

@guangshinhaha
Copy link
Copy Markdown
Collaborator

Summary

  • Add multi-tenancy (School model), OTP-based auth, session management, superadmin dashboard, and school onboarding wizard
  • Fix auth guards across all API routes — replaced getSchoolId() demo fallback with requireSchool() to prevent unauthenticated access
  • Add comprehensive Vitest test suite (58 tests) with real Postgres covering auth, middleware, and all API routes including multi-tenancy isolation

Test Plan

  • 58/58 Vitest tests passing against real Postgres test DB
  • Multi-tenancy isolation verified — school A cannot access school B's data
  • Auth guards verified — unauthenticated requests return 401
  • Admin routes return 403 (not 500) for non-superadmin users

🤖 Generated with Claude Code

claude and others added 18 commits April 2, 2026 11:57
- Add School, User, Session, OtpCode models to Prisma schema
- Add schoolId FK to Teacher, Period, TimetableEntry, SickReport, ReliefAssignment
- Implement email OTP auth (stubbed - logs to console) with session cookies
- Add Next.js middleware for route protection (/dashboard, /admin, /onboarding)
- Update all 11 API routes with schoolId filtering for data isolation
- Create SchoolContext provider for path-aware nav components
- Set up demo mode at /demo/dashboard (unauthenticated, uses demo school)
- Build superadmin dashboard at /admin (create schools, manage users)
- Build school admin onboarding wizard (timetable import + colleague whitelist)
- Update landing page with "Get Started" and "Try Demo" CTAs

https://claude.ai/code/session_012VnoQSMuzvxRdHiJsdibcJ
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…, add factories

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…o shared helpers

- Wrap POST /api/sick-reports in try/catch and return 500 on unexpected errors
- Replace getSchoolId() with requireSchool() in sick-reports, relief-assignments, dashboard, and teachers routes so unauthenticated requests receive 401 instead of silently using the demo school
- Extract mockSession/mockNoSession into src/__tests__/helpers.ts and update all 8 test files to import from there
- Add 401 unauthenticated tests to sick-reports, relief-assignments, teachers (GET+POST), and dashboard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@guangshinhaha guangshinhaha merged commit daa978d into main Apr 2, 2026
1 of 2 checks passed
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
relief-teacher-planning Building Building Preview, Comment Apr 2, 2026 10:53pm

Request Review

guangshinhaha added a commit that referenced this pull request Apr 27, 2026
#111)

Every school interviewed said they want to see a teacher's daily
schedule before assigning relief — the #1 confidence gap in the current
UX. This adds a compact horizontal strip below each candidate's name
in the assign modal showing their full-day timetable at a glance.

Visual: colored blocks per period
- Blue = teaching (hover shows subject + class)
- Amber-light = already covering another relief slot
- Orange/highlighted = the period being assigned (ring indicator)
- Gray = free period

Data pipeline:
- API: enriched allEntriesForDay query with className/subject/period
  number; new schoolPeriods query; buildDaySchedule() helper combines
  timetable + existing relief into per-teacher schedule
- Types: new TeacherPeriodSlot type, daySchedule field on
  AvailableTeacher in both DashboardContent and AssignReliefModal
- Modal: inline strip rendered with flex blocks, hover tooltips,
  and ring indicator on the target period

Scannable in < 2 seconds as KPs work at 6:45am.

Closes #67

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
garygoh-dev pushed a commit that referenced this pull request Apr 27, 2026
Every school interviewed said they want to see a teacher's daily
schedule before assigning relief — the #1 confidence gap in the current
UX. This adds a compact horizontal strip below each candidate's name
in the assign modal showing their full-day timetable at a glance.

Visual: colored blocks per period
- Blue = teaching (hover shows subject + class)
- Amber-light = already covering another relief slot
- Orange/highlighted = the period being assigned (ring indicator)
- Gray = free period

Data pipeline:
- API: enriched allEntriesForDay query with className/subject/period
  number; new schoolPeriods query; buildDaySchedule() helper combines
  timetable + existing relief into per-teacher schedule
- Types: new TeacherPeriodSlot type, daySchedule field on
  AvailableTeacher in both DashboardContent and AssignReliefModal
- Modal: inline strip rendered with flex blocks, hover tooltips,
  and ring indicator on the target period

Scannable in < 2 seconds as KPs work at 6:45am.

Closes #67

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
guangshinhaha pushed a commit that referenced this pull request Apr 30, 2026
Replaces the per-slot cascading-filter algorithm as the default for
internal teacher matching. The new WEIGHTED mode runs in two stages:

  Stage 1 — Rank-Order Centroid (ROC) weighted sum across enabled rules.
  Weights are derived automatically from the admin's drag-to-reorder
  rule order (Barron 1992); no manual tuning. For 6 rules the weights
  are roughly 41% / 24% / 16% / 10% / 6% / 3% — rule #1 dominates but
  isn't absolute, so a candidate strong on multiple lower-rank rules
  can beat one only strong on rule #1. Fixes the small-pool collapse
  problem where strict lex / cascade always pick on rule #1.

  Stage 2 — 2-opt pairwise swap pass over the Stage-1 proposals.
  Catches cross-slot teacher trades — e.g. a teacher who is the only
  Math + 3A match shouldn't get "used up" on a less critical earlier
  slot. Each swap must strictly improve total weighted score and
  satisfy hard constraints (no double-booking, no busy-period
  conflicts, no self-cover). Deterministic, sub-100ms for n ≤ 20.

CASCADE mode (the legacy strict-priority filter) stays available as
an opt-in via School.algoMode = 'CASCADE'. Hard constraints (no
self-cover, no double-book, no assign while sick, hard weekly cap)
are surfaced in the Settings → Algorithm page as locked, uneditable
cards above the editable soft-rule list.
guangshinhaha added a commit that referenced this pull request May 4, 2026
…ives #146) (#149)

* feat(auto-assign): ROC-weighted ranking + 2-opt global swap pass

Replaces the per-slot cascading-filter algorithm as the default for
internal teacher matching. The new WEIGHTED mode runs in two stages:

  Stage 1 — Rank-Order Centroid (ROC) weighted sum across enabled rules.
  Weights are derived automatically from the admin's drag-to-reorder
  rule order (Barron 1992); no manual tuning. For 6 rules the weights
  are roughly 41% / 24% / 16% / 10% / 6% / 3% — rule #1 dominates but
  isn't absolute, so a candidate strong on multiple lower-rank rules
  can beat one only strong on rule #1. Fixes the small-pool collapse
  problem where strict lex / cascade always pick on rule #1.

  Stage 2 — 2-opt pairwise swap pass over the Stage-1 proposals.
  Catches cross-slot teacher trades — e.g. a teacher who is the only
  Math + 3A match shouldn't get "used up" on a less critical earlier
  slot. Each swap must strictly improve total weighted score and
  satisfy hard constraints (no double-booking, no busy-period
  conflicts, no self-cover). Deterministic, sub-100ms for n ≤ 20.

CASCADE mode (the legacy strict-priority filter) stays available as
an opt-in via School.algoMode = 'CASCADE'. Hard constraints (no
self-cover, no double-book, no assign while sick, hard weekly cap)
are surfaced in the Settings → Algorithm page as locked, uneditable
cards above the editable soft-rule list.

* fix(auto-assign): address review of ROC + 2-opt branch (PR #146 revival)

B1 — 2-opt was a no-op for the headline use case. `scoreCandidateForSlot`
called `rule.score(teacher, ctx, [teacher])`. The soft-skip branch in
`match_subject` / `prefer_same_class` (`if (!pool.some(...)) return 1`)
fired for every (teacher, slot) pair because the single-element pool
never matched, so swap deltas were always 0 and the cross-slot trades
the algorithm exists to find never fired. Pass the full teacher pool
through `runTwoOptImprovement` instead.

B2 — `pickedRuleId` reported the dominant ROC contributor, not the
differentiator. With heavy front-loaded weights, a tied rule #1 still
won attribution even when both candidates scored equally on it. Switch
to `argmax weights[i] * (score(picked) - score(runnerUp))` so the
deciding rule is the one that actually separated the pick from the
runner-up. Fixes nonsensical "Lower balance_workload" alternative
strings as a side effect.

C5 — flip rollout default to CASCADE. Schema and migration now default
new schools to the legacy filter; WEIGHTED is opt-in per-tenant via DB
flip until a settings UI ships.

C7 — `AlgorithmTab.handleReset` hardcoded `enabled: true` for every
rule and wiped `config` blocks. Now reads `defaultEnabled` from the
registry and preserves existing config (the workload-window settings
admins set explicitly).

C3 — explicit caveat in `scoreCandidateForSlot` docstring that
`prefer_consecutive` is excluded from Stage 2 because the prior-period
teacher is null; a swap can break a deliberately-consecutive pair.

Tests: Q4 multi-tenant CASCADE/WEIGHTED isolation, Q5 hard_weekly_cap
exclusion in WEIGHTED mode, C4 3-proposal forward-sweep ordering.

---------

Co-authored-by: Claude <noreply@anthropic.com>
@garygoh-dev garygoh-dev deleted the claude/reliefcher-new-feature-IViMe branch May 7, 2026 16:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants