# Wiring the frontend to this API

The prototype in `../KPOD-FrontEnd` keeps everything in memory:

| What | Where | Replaced by |
| --- | --- | --- |
| The portfolio | `createInitialSites()` in `src/data/seed.ts` | `GET /sites` |
| Every mutation | `appReducer` in `src/store/appReducer.ts` | the write endpoints |
| Auth | `AuthProvider` + `DEMO_USERS` in `src/store/auth-context.ts` | `POST /auth/login` |
| Derived metrics | `src/utils/calc.ts` | sent down on each payload |

The API was shaped so this is a swap, not a rewrite. Field names on the wire
are the frontend's own (`prio`, `pStart`, `choiceVal`, `ops`, `opsLog`), so
components that read a site keep working untouched.

## 1. Point the app at the API

```
# .env.local
VITE_API_URL=http://localhost:8000/api/v1
```

The backend allows `http://localhost:5173` and `http://127.0.0.1:5173` by
default; set `CORS_ALLOWED_ORIGINS` in the backend `.env` for anything else.

## 2. Auth

`AuthProvider` becomes a thin wrapper over the token endpoints:

- `signIn(email, password, remember)` → `POST /auth/login`, store
  `data.token`. `remember` picks `localStorage` vs `sessionStorage` exactly as
  it does now, and also tells the server whether to issue an expiring token.
- `readStoredSession()` → `GET /auth/me` on boot. A 401 means the token is
  gone; clear it and drop to the login screen.
- `signOut()` → `POST /auth/logout`, then clear storage.
- `requestReset(email)` → `POST /auth/forgot-password`. It already always
  resolves; that stays true.

## 3. Role and capabilities

`RoleSelector` and `readStoredRole()` are a testing affordance — the role was
a client-side toggle because there was no server. The login response now
carries the real thing:

```json
{ "role": "admin", "capabilities": ["task.write", "site.edit", "site.addImage", "ops.log"] }
```

Feed `capabilities` into `allows()` instead of deriving it from
`CAPABILITIES[role]`. The strings are identical on both sides on purpose, so
`allows('site.edit')` and the route's `permission:site.edit` middleware cannot
drift apart. Keep `utils/permissions.ts` for the labels.

The role selector should come out for production — a user cannot promote
themselves any more, and the server will refuse it either way.

## 4. Reducer cases → endpoints

| Reducer case | Call |
| --- | --- |
| `project/add` | `POST /programs` |
| `taskType/add` | `POST /task-types` |
| `site/add` | `POST /sites` |
| `site/patchInfo` | `PATCH /sites/{code}` |
| `site/addImage` | `POST /sites/{code}/images` |
| `site/setReadiness` | `PATCH /sites/{code}/readiness` |
| `task/save` | `PUT /tasks` |
| `task/setChoice` | `PATCH /sites/{code}/tasks/{key}/choice` |
| `task/setYesNo` | `PATCH /sites/{code}/tasks/{key}/yes-no` |
| `task/setStage` | `PATCH /sites/{code}/tasks/{key}/stages` |
| `ops/logMetric` | `POST /sites/{code}/ops-log` |
| `ops/logSubmission` | `POST /sites/{code}/tasks/{key}/submissions` |

`filters/*`, `project/set` and `role/set` stay client-side — they are view
state. Pass the filters to `GET /sites` as query parameters instead of
filtering in `filterSites`.

Every write returns the **updated site** (or sites, for `PUT /tasks`), so the
handler can splice the response straight into state rather than refetching.

## 5. Derived values

`calc.ts` recomputes on every render. The API now sends the same numbers, and
they are covered by tests that pin them to the prototype's own output:

| `calc.ts` | On the wire |
| --- | --- |
| `siteOverall` | `site.overall` |
| `siteReady` | `site.ready` |
| `siteDisplayStatus` | `site.displayStatus` |
| `siteNeedsAttention` | `site.needsAttention` |
| `siteR2sDone` | `site.r2sDone` |
| `sitePct` | `site.pct` |
| `siteDeliveryAvg` | `site.deliveryAvg` |
| `siteCurrentTask` | `site.currentTask` |
| `siteOpsBoxes` | `site.opsBoxes` (detail only) |
| `deliveryItems` / `deliveryPct` | `site.delivery` (detail only) |
| `boardMetrics` | `GET /dashboard/executive` → `data.board` |
| `documentFunnel` / `funnelMax` | `data.funnel` |
| `rankSites` | `data.leaderboard.top` / `.bottom` |
| `buildKpis` | `GET /dashboard/program` → `data.kpis` |
| `opsSnapshot` / `opsTotals` | `data.opsSnapshot` / `data.opsTotals` |

Keeping them client-side would also work — the inputs are all in the payload.
Reading them off the response is what stops the map, the table and the
dashboards from ever disagreeing.

`ganttScale` stays client-side: it depends on the viewport, not the data.

## 6. Things that change behaviour

- **Site codes are generated server-side.** `makeSiteId` numbers by total site
  count, which collides the moment two sites share a three-letter prefix. The
  server numbers per prefix and skips what is taken, so `Kirkuk A` and
  `Kirkuk B` come back as `KIR-01` and `KIR-02`. Don't predict the code — read
  it off the response.
- **Metric/site-type mismatches are now rejected.** Logging `actScan` against a
  document site is a 422 rather than a silently stored figure. The forms
  already only offer valid metrics, so this only bites on a hand-built request.
- **Multi-site saves are all-or-nothing.** One bad code fails the request; the
  prototype would quietly skip it.
- **`TODAY` is pinned in the prototype** (`2026-07-20`) so its timelines are
  reproducible. The server stamps real dates on writes but seeds history
  against the same pinned date (`KPOD_TODAY`). Drop the pin on both sides
  together, or the today-marker and the newest log entries will disagree.
- **The gallery is real rows now.** `galleryCount` still comes down, but each
  slot has an id and a nullable `url`, ready for actual uploads.

## 7. Getting a backend up

```bash
cd kpod-backend
composer install
php artisan migrate:fresh --seed
php artisan serve            # http://localhost:8000
```

The seed reproduces the prototype's exact demo portfolio — same 11 sites, same
task histories, same figures — because the seeder ports `data/seed.ts`
including its deterministic PRNG. `tests/Unit/DemoRandomTest.php` pins that
port against values captured from the frontend's own `utils/math.ts`.
