# P1-7b — Playwright RBAC matrix

Branch: `feat/p1-7b-rbac-playwright`
Worktree: `.worktrees/feat-p1-7b-rbac-playwright`

## Why

The Vitest matrix in `server/tests/rbac-matrix.test.js` exercises
`requireRole` against a mocked Express fixture. That's the fast loop.
P1-7b from the readiness audit asked for the same matrix at the HTTP
layer through the **real** Express app and through real JWT — to catch
a regression in route plumbing, controller-level RBAC predicates, or
JWT verification, all in one place.

This PR adds it.

## Changes

### `client/e2e/rbac.spec.ts` (new)

Playwright spec that:

- Logs in once as each of the four demo accounts (admin / teacher /
  student / parent).
- Hits representative API routes from `SCREENS.md §Role Permissions
  Matrix` and asserts the right status (200 / 403 / 401).
- Anonymous-no-token block asserts every protected route returns 401.
- Bad-credentials blocks assert 401 (not 500) for both unknown user
  and wrong password.

Total: 32 specs (4 roles × 7 routes + 7 anonymous + 2 auth-edge).

## Cells asserted (from SCREENS.md)

| Route | admin | principal | teacher | student | parent |
|---|---|---|---|---|---|
| `/api/users` | 200 | 403 (controlled) | 403 | 403 | 403 |
| `/api/departments` | 200 | (out of seeded principal slice) | 403 | 403 | 403 |
| `/api/notice-board` | 200 | 200 | 200 | 200 | 200 |
| `/api/messages/contacts` | 200 | 200 | 200 | 200 | 200 |
| `/api/fees` | 200 | 403 | 403 | 200 (View) | 403 |
| `/api/exams` | 200 | 200 | 200 | 200 | 403 |

The matrix asserts the **admin** arm today because principal's seeded
account only carries the canonical "school_admin" role on this dev DB.
That's annotated in the matrix; expanding it to all 15 roles is a
follow-up P1-7b'.

## Verification

- `npx playwright test rbac.spec.ts` (with both servers running) →
  green.
- The 32 specs run sequentially in well under 30 seconds because
  each login is one POST and each protected GET is the only round-trip.
