# Staff Court Monitor (Mobile) — API Contract

> Every endpoint this role consumes. Consistent with `FOUNDATION_SPEC §2` conventions: Bearer JWT, **runtime RBAC resolver** (reads `RolePermission` per request — no hardcoded `if role==='X'`), **Game-Zone scoping** (Monitor = own GZ + own assignments), audit-row-on-write, pagination, `If-Match`/ETag → 409, multipart photo upload, standard error envelope.
> `Roles` column lists the roles whose **resolved permission** allows the call — it is documentation of the seed matrix, NOT a hardcoded gate. The Monitor's effective access is "own roster / own assigned checklists / own Game Zone".
> Base path: `/api/v1`. The Flutter app consumes these via generated Dart models (`packages/types` → OpenAPI → Dart).

---

## Auth

| Method | Path | Roles | Body | Returns |
|---|---|---|---|---|
| POST | `/auth/login` | public | `{ identifier, password }` (email or phone) | `{ accessToken, refreshToken, expiresIn }` · 401 bad creds · 423 locked · 429 rate-limited |
| POST | `/auth/refresh` | public (valid refresh) | `{ refreshToken }` | `{ accessToken, refreshToken, expiresIn }` (rotated; reuse → 401 + revoke) |
| POST | `/auth/logout` | any | `{ refreshToken }` | `204` (refresh revoked) |
| POST | `/auth/forgot-password` | public | `{ identifier }` | `204` (always; no account enumeration) |
| POST | `/auth/reset-password` | public (valid token/OTP) | `{ token, newPassword }` | `204` · 422 weak password |
| POST | `/devices/register` | any | `{ platform, fcmToken }` | `201 { id }` (upserts `DeviceToken` for FCM push) |

## Bootstrap / identity

| Method | Path | Roles | Body | Returns |
|---|---|---|---|---|
| GET | `/me` | any | — | `{ user, role:{code:"MON",...}, gameZoneScope:"own", gameZone, reportsTo, certifications:[ride], nav:[...5 bottom-nav slots...], permissions:[...] }` — **nav + scope are server-resolved** |
| GET | `/me/profile` | any | — | `{ fullName, email, phone, role, gameZone, reportsTo, certifiedRides:[...] }` (backs `SH-PROFILE`) |

## My roster (`MON-ROSTER-OWN`)

| Method | Path | Roles | Body | Returns |
|---|---|---|---|---|
| GET | `/me/roster?date=YYYY-MM-DD` | MON (own) | — | `{ entries:[{ id, date, ride, shift:{name,start,end}, gameZone }] }` — own entries only |
| GET | `/me/roster?weekOf=YYYY-MM-DD` | MON (own) | — | `{ entries:[...7 days...] }` (day/week toggle) |

> Read-only. Monitor has no roster create/edit permission — those endpoints (in SM/TL packs) reject with 403 for MON via the resolver.

## My assigned checklists for the shift (`MON-DASH`, `MON-CHK-TODAY`)

| Method | Path | Roles | Body | Returns |
|---|---|---|---|---|
| GET | `/me/checklists/today` | MON (own) | — | `{ instances:[{ id, template:{name,frequency}, ride, itemCount, state, dueAt, photosNeeded:bool }] }` — own assignments, own GZ; `state` is the stored sub-state, status rendered client-side via the same rules |
| GET | `/me/dashboard` | MON (own) | — | `{ shift:{ ride, name, start, end }, counts:{ assigned, inProgress, submitted, done, overdue }, instances:[...] }` |

## Checklist instance + items detail (`MON-CHK-FILL`)

| Method | Path | Roles | Body | Returns |
|---|---|---|---|---|
| GET | `/checklist-instances/:id` | MON (own filler) | — | `{ id, template, ride, gameZone, periodStart, periodEnd, dueAt, state, version, sections:[{ title, items:[{ id, text, testMethod, requiresPhotoOnA, response:{value,note,recordedAt,initials}|null, aPhoto:{id,url}|null }] }], completionPhotos:[{id,url,thumbUrl}] }` · 403 if not the assigned filler / out of GZ |

## Save / submit responses (`MON-CHK-FILL`, `MON-CHK-SUBMIT`)

| Method | Path | Roles | Body | Returns |
|---|---|---|---|---|
| PATCH | `/checklist-instances/:id/responses` | MON (own filler) | `If-Match: <version>` · `{ responses:[{ itemId, value:"G"|"A", note?, initials?, recordedAt? }] }` | `200 { saved, state:"IN_PROGRESS", version }` · 409 stale version · sets `fillerId` on first save |
| POST | `/checklist-instances/:id/submit` | MON (own filler) | `If-Match: <version>` | `200 { state:"SUBMITTED", submittedAt, approvalSteps:[TL,SM,OH] }` · **422** `{ fields:{ completionPhoto?, items:[...untouched/missing-A-photo...] } }` if the photo/answer gate fails · fires TL notification |

> **Submit gate (server-enforced, §4a/L7):** rejects with 422 unless every item has a response, ≥1 `COMPLETION` photo exists, and every `A` item has a `NEGATIVE` photo. The UI mirrors this but never bypasses it.

## Photo upload (completion + per-A-item) (`MON-CHK-FILL`)

| Method | Path | Roles | Body | Returns |
|---|---|---|---|---|
| POST | `/uploads/photos` | MON (own filler) | `multipart/form-data`: `file`, `instanceId`, `itemId?` (set ⇒ per-A-item), `kind:"COMPLETION"|"NEGATIVE"`, `device?` | `201 { id, url, thumbUrl, kind, itemId? }` — stored to **local disk on VPS** |
| DELETE | `/uploads/photos/:id` | MON (own uploader) | — | `204` (remove a staged photo before submit) |

## My submissions + approval status (`MON-CHK-STATUS`)

| Method | Path | Roles | Body | Returns |
|---|---|---|---|---|
| GET | `/me/submissions?status=` | MON (own) | — | `{ instances:[{ id, template:{name}, ride, state, submittedAt, currentStep, sentBackReason? }] }` — own submissions; `status` filter accepts `active`/`sent_back`/`done` |
| GET | `/checklist-instances/:id/timeline` | MON (own) | — | `{ steps:[{ level, role, actor, at, state }], sentBackReason? }` — backs the ApprovalTimeline; Monitor sees real sub-states, never "Pending" |

## Notifications (`SH-NOTIF`, top-bar bell)

| Method | Path | Roles | Body | Returns |
|---|---|---|---|---|
| GET | `/me/notifications` | any | — | `{ items:[{ id, type:"CHECKLIST_DUE"|"SENT_BACK"|"ROSTER_PUBLISHED", title, body, at, read }] }` |
| POST | `/me/notifications/:id/read` | any | — | `204` |

---

## Notes
- **No approve/verify endpoints** appear here — Monitor cannot approve (`§3`). `POST /checklist-instances/:id/approve` and `/sendback` live in the TL/SM/OH packs; the resolver returns 403 for MON.
- **Send-back is received, not sent:** when a TL/SM/OH sends back, the cascade engine sets the instance to `SENT_BACK` and notifies the **original filler (Ramesh)** — surfaced via `/me/notifications` + `/me/submissions` (`HANDOFFS.md`).
- All write endpoints emit an `AuditLog` row (`FOUNDATION_SPEC §2`).
- Every read is Game-Zone + own-assignment scoped server-side; passing another user's instance id → 403.
