# Senior 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** (SCM = own GZ + own assignments; + own team for the read-only Team view), 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 Senior Monitor's effective access is "own roster / own assigned checklists / own Game Zone" + a **read-only** team view; it has **no approve/verify** permission.
> 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:"SCM",...}, gameZoneScope:"own", gameZone, reportsTo, certifications:[ride], nav:[...5 bottom-nav slots...], permissions:[...incl. read-only checklist.view team scope...] }` — **nav + scope are server-resolved** |
| GET | `/me/profile` | any | — | `{ fullName, email, phone, role, gameZone, reportsTo, certifiedRides:[...] }` (backs `SH-PROFILE`) |

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

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

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

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

| Method | Path | Roles | Body | Returns |
|---|---|---|---|---|
| GET | `/me/checklists/today` | SCM (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` | SCM (own) | — | `{ shift:{ ride, name, start, end }, counts:{ assigned, inProgress, submitted, done, overdue }, instances:[...], teamSummary:{ monitors, submitted, sentBack, overdue } }` — `teamSummary` backs the Home Team-status card |

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

| Method | Path | Roles | Body | Returns |
|---|---|---|---|---|
| GET | `/checklist-instances/:id` | SCM (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 (`SCM-CHK-FILL`, `SCM-CHK-SUBMIT`)

| Method | Path | Roles | Body | Returns |
|---|---|---|---|---|
| PATCH | `/checklist-instances/:id/responses` | SCM (own filler) | `If-Match: <version>` · `{ responses:[{ itemId, value:"G"|"A", note? }] }` | `200 { saved, state:"IN_PROGRESS", version }` · 409 stale version · sets `fillerId` on first save · **`recordedAt` = server time, `initials` auto-derived (§9 #9) — not client-supplied** |
| POST | `/checklist-instances/:id/submit` | SCM (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/§9 #7/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) (`SCM-CHK-FILL`)

| Method | Path | Roles | Body | Returns |
|---|---|---|---|---|
| POST | `/uploads/photos` | SCM (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` | SCM (own uploader) | — | `204` (remove a staged photo before submit) |

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

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

## Team / Zone status — READ-ONLY (`SCM-TEAM-STATUS`) — ADDED

| Method | Path | Roles | Body | Returns |
|---|---|---|---|---|
| GET | `/me/team/checklists/today` | SCM (own GZ + team, **read**) | — | `{ monitors:[{ user:{id,fullName,initials}, ride, shift, counts:{assigned,submitted,sentBack,overdue}, instances:[{ id, template:{name,frequency}, state, dueAt }] }] }` — Staff Court Monitors in the SCM's Game Zone + same team; `state` rendered via shared `renderStatusForViewer()` (SCM = real sub-state, never "Pending") |
| GET | `/checklist-instances/:id` | SCM (own GZ + team, **read**) | — | read-only instance detail (same shape as the filler GET) for a **team** instance — photos + responses + timeline; **no write endpoints exposed** |
| GET | `/checklist-instances/:id/timeline` | SCM (own GZ + team, **read**) | — | the cascade timeline for a team instance (read-only) |

> **No approve / send-back endpoint is consumed here.** The resolver grants SCM a read-only `checklist.view` with `scope=own_team`; `checklist.approve` is **denied** (403). `POST …/approve` and `…/sendback` live only in the TL/SM/OH packs.

## 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 — Senior Monitor cannot approve (`§3`). `POST /checklist-instances/:id/approve` and `/sendback` live in the TL/SM/OH packs; the resolver returns 403 for SCM.
- **The Team view is read-only:** SCM is granted `checklist.view` (team scope) only; every team endpoint is a GET. There is no SCM write path into another monitor's instance.
- **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 (Karan)** — surfaced via `/me/notifications` + `/me/submissions` (`HANDOFFS.md`).
- All write endpoints emit an `AuditLog` row (`FOUNDATION_SPEC §2`).
- Every read is Game-Zone + scope-filtered server-side; passing another user's instance id outside own-team → 403.
