# Phase 2 + Phase 3 Rollout Plan — 2026-07-22 **Date:** 2026-07-22 **Owner:** fchin (orchestrator) — Arthur owns Phase 2 implementation, Craig owns Phase 3 implementation **Source plans:** [phase2-analytics](./2026-07-22-phase2-analytics.md) · [phase3-offboarding](./2026-07-22-phase3-offboarding.md) **Decision (this doc):** salvage + rebase for Phase 2; both phases wait for Phase 1 to land before starting ## 1. State of play (today) | Branch | Worktree | State | |---|---|---| | `dev` (`f50e272`) | main checkout | Uncommitted modifications in `App.tsx`, `Nav.tsx`, `Reports.tsx`, `docker-compose.yml`, `Dockerfile`, `docker-entrypoint.sh`, `reports.controller.js`, `demo.js` seed. **Dirty — leave alone.** | | `feature/admin-cohorts-2026-07-22` | `.worktrees/feat-auto-20260722-ed7cf80c/` | **Phase 1 in flight.** `user_roles` PR committed (changelog + migration + controller + components + e2e spec). `cohorts` PR partially staged (uncommitted: `cohorts.controller.js`, `cohorts` migration, `SyncEngine.js`, `index.js`, plus `dbg12.js` / `seed_smoke.js` debug noise to drop). Class/teacher assignment UX not started. | | `feature/phase2-analytics` (`5eb6729`) | `.worktrees/phase2-analytics/` | **Phase 2 donor branch.** Branched from `4c0ab8b` (older dev, pre-superadmin). 2 commits of analytics work: `fb80a73` KPI rebuild + `5eb6729` 100%-real-SQLite follow-up. Result: 812-line `Reports.tsx`, 548-line `reports.controller.js`. Predates the Phase 1 schema and the new plan doc; missing the `principal`/`bursar`/`hr` role-scoped access from the plan doc. | | _(none)_ | _(none)_ | **Phase 3 has no worktree yet.** | **Solo-dev rule (AGENTS.md):** "only one worktree at a time per developer." Phase 2 + Phase 3 are file-disjoint but cannot run truly in parallel on this machine. The work plan below sequences Phase 2 first (smaller, closer to done) then Phase 3. ## 2. Sequencing ``` dev (current, dirty) │ ├─► finish + merge Phase 1 → feature/admin-cohorts-2026-07-22 into dev │ ├─► Phase 2 worktree from clean dev → 3 PRs into dev │ (PR1 backend endpoints, PR2 frontend rewrite, PR3 routing fix + e2e) │ ├─► Phase 3 worktree from clean dev → 4 PRs into dev │ (PR1 migration + service + unit tests, PR2 controller, │ PR3 frontend pages, PR4 offboard buttons + e2e) │ └─► promote dev → main (only after both phases green + Phase 1 green) ``` **Why this order:** Phase 1's `user_roles` and `student_cohorts` tables are referenced by Phase 2 (`/api/reports/cohorts`, `principal` reads via the new `hasRole` helper) and Phase 3 (`OffboardingService` calls the user_roles revoke logic). Phase 2 is smaller and reuses existing `reports.controller.js`; Phase 3 introduces 2 new tables and the transaction-critical `OffboardingService`. Phase 2 first gives Arthur a quick win and surfaces any App.tsx/Nav.tsx merge issues before Phase 3 needs to merge into the same files. ## 3. Phase 2 — worktree: `.worktrees/analytics-dashboard-2026-07-22/` **Branch:** `feature/analytics-dashboard-2026-07-22` (renamed from `feature/phase2-analytics` for plan-doc alignment) **Setup (after Phase 1 lands in `dev`):** ```bash cd .worktrees/phase2-analytics git remote prune origin git branch -m feature/phase2-analytics feature/analytics-dashboard-2026-07-22 git fetch origin git merge origin/dev # not rebase — preserves the 2 donor commits on top # resolve conflicts in: client/src/App.tsx, client/src/components/Nav.tsx, # client/src/store/auth.ts, server/src/database/seeds/demo.js git worktree move .worktrees/phase2-analytics .worktrees/analytics-dashboard-2026-07-22 ``` **Work to complete (gap-fill against the plan doc):** | Plan-doc item | Status in donor branch | Action | |---|---|---| | `/api/reports/kpis` endpoint | Likely already present (large `reports.controller.js`) | Verify against the documented shape; add role-scoped filtering if missing | | `/api/reports/timeseries` | Likely already present | Verify `metric`/`range`/`granularity` query handling; add if missing | | `/api/reports/cohorts` | Probably gated by Phase 1's `student_cohorts`; verify not 500ing | Add/fix; tie to Phase 1's cohort schema | | `/api/reports/export` generalization | Existing `GET /api/reports/export/:type` per the conventions doc | Generalize to `?type=` query param, stream CSV, support 4 new types | | `KpiTile` / `DrilldownModal` / `FilterBar` components | Probably inline in 812-line `Reports.tsx` | Extract as separate components per the plan doc | | 5 tabs (Overview/Academics/Cohorts/Finance/Staff) with role gating | TBD — verify | Add missing tabs; wire role visibility to `hasRole` from Phase 1 | | CSV export button | Probably exists | Verify; ensure dropdown covers all 4 types | | `principal`/`bursar`/`hr` in every `getRoutes()` arm of `App.tsx` for `/reports` | **Missing — primary gap** | Add `principal` to every arm; add `bursar`/`hr` to arms where role should see the route | | Cohorts tab conditional on Phase 1 | If Phase 1 ships first, no flag needed | If Phase 1 is delayed, gate with `featureFlagCohorts` query param | | Changelog | Missing | Write `.harness/changelogs/2026-07-22-analytics-dashboard.md` | **Verification bar (cross-walked from plan doc §Verification):** 1. `git diff --stat server/src/database/` is empty for this PR (no new tables). 2. `GET /api/reports/kpis?range=6m` returns documented shape with non-zero seed values. 3. `GET /api/reports/timeseries` returns 6+ points for 6m, 26+ for 6m+week. 4. `GET /api/reports/cohorts` returns rows from the Phase 1 schema (or `[]` if Phase 1's cohorts are empty on the seed). 5. `GET /api/reports/export?type=...&format=csv` downloads a non-empty CSV for each of Overview/Academics/Cohorts/Finance. 6. Role gating verified: `school_admin`/`principal` see all 5 tabs, `bursar` sees 4 (no Staff), `hr` sees 4 (no Finance), `student`/`parent`/`teacher` get 403. 7. Time-range filter changes data (set 7d vs 6m). 8. Cohort drilldown opens modal with ranked students; Esc closes. 9. Every KPI query + time-series query <500ms; EXPLAIN QUERY PLAN checked on seed. 10. E2E spec: load `/reports` as `school_admin`, all 5 tabs render, range switch, drilldown, CSV export. Existing portal-smoke tests pass. 11. No regression on `admin/principal dashboards`, exam-review, finance module. **Execution order (this plan only):** ``` PR 1: backend endpoints (kpis, timeseries, cohorts, export) — gap-fill on reports.controller.js PR 2: frontend — extract KpiTile/DrilldownModal/FilterBar; complete 5 tabs; role gating PR 3: App.tsx routing fix (principal/bursar/hr for /reports) + E2E spec + changelog ``` **Merge into dev:** after PR 3 is green and the verification bar is met. ## 4. Phase 3 — worktree: `.worktrees/offboarding-2026-07-22/` **Branch:** `feature/offboarding-2026-07-22` (new branch off post-Phase-1 dev) **Setup (after Phase 2 lands in `dev`):** ```bash cd /c/Users/fchin/Documents/next-gen git fetch origin git worktree add .worktrees/offboarding-2026-07-22 -b feature/offboarding-2026-07-22 origin/dev cd .worktrees/offboarding-2026-07-22 ``` **Work to complete (per the plan doc, with concrete PR sequencing):** **PR 1 — schema + service (foundation, hardest part):** - `server/src/database/migrations/knex/2026072200000030_offboarding.js` — `offboarding_records`, `offboarding_actions`, `ALTER TABLE users ADD COLUMN archive_status`. - `server/src/services/OffboardingService.js` — `offboardStudent()` + `offboardStaff()` pipelines. Both wrap in `db.transaction()`. Reuse Phase 1's `user_roles.revoked_at` revoke logic. - Local node script `server/test_offboarding_service.js` exercising the happy path + rollback-on-step-failure path. Deleted before merge. **PR 2 — controller + sync + routing:** - `server/src/controllers/offboarding.controller.js` — all 7 endpoints from the plan doc. - `server/src/index.js` — register the controller. - `server/src/services/SyncEngine.js` — append `offboarding_records` + `offboarding_actions` to `tablesToSync` (parents first: `users` → `enrollments` → `offboarding_records` → `offboarding_actions`). **PR 3 — frontend pages + components:** - `client/src/store/offboarding.ts` — Zustand slice; uses `client/src/store/api.ts` axios instance. - `client/src/pages/admin/Offboarding.tsx` — list view with status/cohort/date filters. - `client/src/pages/admin/OffboardingWizard.tsx` — 4-step wizard. - `client/src/pages/admin/Alumni.tsx` — read-only directory. - `client/src/components/OffboardingStepRow.tsx`, `OverrideModal.tsx`. - Routes + Nav entries in `client/src/App.tsx`, `client/src/components/Nav.tsx`. **PR 4 — wire-up + e2e + changelog:** - "Offboard" buttons on `Users.tsx` (student detail), `HRManagement.tsx`, `StaffDirectory.tsx`. - E2E spec (`client/e2e/offboarding.spec.ts`) covering: student happy path, student fee block + override, staff happy path, cancel. - `.harness/changelogs/2026-07-22-offboarding.md`. **Verification bar (cross-walked from plan doc §Verification):** 1. Migration up + down clean on both fresh DB and existing DB. 2. Student happy path: `users.is_active=0`, `archive_status='active_alumni'`, all 5 enrollment/hostel/transport/club state mutations land, 11 `offboarding_actions` rows all `completed`, 1 `audit_log` row. 3. Student fee block: outstanding balance → step 5 marked `failed`; with `override.reason` → step 5 `completed` + 1 extra `audit_log` row tagged `override`. 4. Staff happy path: `is_active=0`, `archive_status='former_staff'`, `classes.class_teacher_id` and `subjects.teacher_id` set to `NULL`, pending `leave_requests` cancelled, payroll block skipped if last run paid. 5. Payroll block: unpaid last run → step 2 failed; with override → completed + audit row. 6. Transaction integrity: force step 4 failure → entire pipeline rolls back (no `users.is_active` flip, no `offboarding_actions` rows, no `enrollments` updates). 7. Cancel: `status='cancelled'`, partial `offboarding_actions` retained as history. 8. Sync: each new table sync-tested in isolation; full `tablesToSync` sweep clean. 9. E2E: 4 specs pass; existing portal-smoke regression-clean. 10. No regression on admin/principal dashboards, exam-review, finance, analytics. ## 5. Merge coordination **Files touched by both phases (potential conflicts at merge into dev):** | File | Phase 2 | Phase 3 | |---|---|---| | `client/src/App.tsx` | Adds `principal`/`bursar`/`hr` to `/reports` allowed roles; potentially adds a nav entry | Adds routes for `/offboarding`, `/offboarding/wizard`, `/alumni`; Nav entries | | `client/src/components/Nav.tsx` | Same — analytics nav entry already exists | Adds Offboarding + Alumni nav entries | | `server/src/index.js` | No change | Registers `offboarding` controller | | `server/src/services/SyncEngine.js` | No change | Appends 2 tables | **Strategy:** Phase 2 merges first (no `SyncEngine.js`/`index.js` change). When Phase 3 merges, its `App.tsx`/`Nav.tsx` changes will be on top of Phase 2's — simple additive merge in those files; no conflict on `SyncEngine.js` or `index.js`. ## 6. Risks + watch-points - **Phase 1's uncommitted cohorts PR must land first.** The `server/src/database/seeds/demo.js` modifications in `dev` (uncommitted) suggest Arthur may be staging demo seed updates — confirm with Phase 1 owner before starting Phase 2. - **Debug files in Phase 1 worktree** (`server/dbg12.js`, `server/seed_smoke.js`) must not land in dev. The Phase 1 PR review should call these out. - **Phase 2's principal/bursar/hr routing fix may conflict with Phase 1's `App.tsx` additions** (Phase 1 already adds `principal` in places). Verify with `git grep -n "principal" client/src/App.tsx` after each merge. - **Phase 3's transaction integrity is the load-bearing risk.** Without a test framework, the rollback verification (verification item 6) must be exercised manually before each Phase 3 PR merge. - **No lint/typecheck scripts** — Phase 2 frontend rewrite and Phase 3 new TS files will compound the existing "73 TypeScript warnings" issue. Don't make it materially worse; the project compiles with `strict: false` already. - **CSV export is the only export format.** PDF export is explicitly deferred per both plan docs. - **Cohorts tab in Phase 2 is conditional on Phase 1's `student_cohorts` table** — if for any reason Phase 1 ships but the cohorts migration doesn't apply cleanly, the tab silently renders empty rather than 500s. Add a feature-flag query param if needed.