# 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.