# Chavivim Division Overrides API — integration guide

> 🆕 **New — added 2026-06-23.** This whole resource (`/api/v1/divisions/*` and the `division:*` scopes) is new.

For developers wiring the **coordinator mobile app** (or the central dispatch/portal) so a division coordinator can edit their division's headline **stats** and **fleet vehicle photos** — without a code deploy. Pair this with the interactive spec on this site (`openapi.yaml`, the Swagger page) — this guide is the prose + copy-paste; Swagger is for trying calls live.

---

## TL;DR

- **Base URL:** `https://worker.chavivim.org/api/v1` — the authenticated API is served **only** on this dedicated worker host (plus the `*.workers.dev` staging host), **not** on the `chavivim.org` apex or the division domains (those return `404`).
- **Auth:** `Authorization: Bearer cvk_…` on **every** request. Keep the token server-side.
- **What it edits:** a division's `annual_calls`, `volunteer_count`, `vehicle_count`, and per-vehicle **fleet photos**. Each edit lands in D1 and **wins over** the compiled `tenants/<slug>.ts` config at render time (the same "D1 row beats static" rule the blog posts use). Leave a field unset and the site renders the compiled value exactly as it does today — **nothing changes until you write an override.**
- **`:slug`** is an **internal division slug** (`kj`, `rockland`, `catskills`, …) — **not** `shared` (rejected with `404`).
- **Cross-tenant token, consumer-enforced tenancy.** One bearer token authenticates *the app* and can edit **any** division; **your app** is responsible for restricting each coordinator to their own division(s). Send an optional `actor` (the coordinator's email) on writes for per-user audit.
- **Format:** JSON in / JSON out. The fleet-photo upload is `multipart/form-data`.

---

## Scopes

`division:read`, `division:write` (or `division:*`). A `403` with `code: "insufficient_scope"` means the key lacks the scope.

| action | scope |
|---|---|
| `GET` the merged division view | `division:read` |
| `PATCH` stat overrides, upload a fleet photo | `division:write` |

A key for a coordinator app typically holds `division:read` **and** `division:write` (or `division:*`). Mint keys at `worker.chavivim.org/admin/api-keys`.

---

## The merged division view

`GET` and `PATCH` both return the same `{ "division": { … } }` — the compiled config with any D1 overrides applied, plus flags telling you which fields are currently overridden (so a UI can show "edited" badges and a "reset" affordance):

```jsonc
{ "division": {
  "slug": "kj",
  "org_name": "Chavivim Orange County",
  "stats": {                              // effective values (override ?? compiled)
    "annual_calls": 17000,
    "volunteer_count": 120,
    "vehicle_count": 3
  },
  "overridden": {                         // true = a D1 override is in effect
    "annual_calls": false,
    "volunteer_count": false,
    "vehicle_count": false
  },
  "vehicles": [
    { "id": "kj-500", "code": "KJ-500", "name": "…",
      "image_url": "/assets/kj/kj-500.jpg",   // override if set, else compiled imagePath
      "image_overridden": false }
  ],
  "updated_at": null,                     // unix ms of the last stat override, or null
  "updated_by": null                      // "<actor> (api:<keyname>)" or "api:<keyname>", or null
}}
```

---

## Endpoints & flows (copy-paste)

`$T` = your token (set `Authorization: Bearer $T` on every call; omitted below for brevity). `$B` = `https://worker.chavivim.org/api/v1`.

### Read the current (merged) view
```
GET /divisions/{slug}                              (scope division:read)
→ { "division": { …see above… } }
```
Load this to populate your editor: render each stat with an "edited" badge where `overridden.<field>` is `true`, and each vehicle with its `image_url` thumbnail + a "replace photo" control. Use `vehicles[].code` as the upload key (below).

### Edit a division's stat overrides
```
PATCH /divisions/{slug}                            (scope division:write)
{ "annual_calls": 18000, "volunteer_count": null, "actor": "coordinator@example.org" }
→ { "division": { …refreshed merged view… } }
```
- Send **any subset** of `annual_calls`, `volunteer_count`, `vehicle_count`.
- A **number** sets the override; **`null`** clears it (back to the compiled default); **omitting** a field leaves it unchanged.
- Values must be **non-negative integers**. A bad value or an unknown body key → `400 invalid_field`. No editable field supplied → `400 empty_patch`.
- `actor` (optional) is recorded for audit; it is not a stat.

### Replace a fleet vehicle photo
```
POST /divisions/{slug}/vehicles/{code}/image       (scope division:write)
Content-Type: multipart/form-data        (part "file"; optional "actor")
→ 201 { "image_url": "/media/kj/fleet/kj-500-…",
        "tenant": "kj", "vehicle_code": "KJ-500",
        "division": { …refreshed merged view… } }
```
- `{code}` is the **exact, case-sensitive** vehicle badge, e.g. `KJ-500` (take it from `vehicles[].code`). An unknown code → `404`.
- Max **15 MB**; `jpeg/png/webp/gif/heic/heif`.
- The image is stored in R2 under `/media/<slug>/fleet/…` and immediately replaces that vehicle's photo on the division **home** fleet-preview and the **fleet** page.

```sh
curl -X POST "$B/divisions/kj/vehicles/KJ-500/image" -H "Authorization: Bearer $T" \
  -F "file=@/path/kj-500.jpg" -F "actor=coordinator@example.org"
```

---

## A typical coordinator-app flow

1. Coordinator opens "My division" → app calls `GET /divisions/{their slug}` with a `division:read`+`division:write` key.
2. App renders the editable stats (with "edited" badges from `overridden`) and the fleet list with thumbnails.
3. On **Save** → `PATCH /divisions/{slug}` with only the changed fields + `actor`. Re-render from the returned `division`.
4. On **Replace photo** → `POST …/vehicles/{code}/image`; re-render from the returned `division`.
5. To **revert** a stat to the compiled default → `PATCH` that field to `null`.

> **Restrict by division in YOUR app.** The token is cross-tenant — it can edit any `:slug`. Authenticate the coordinator and only ever send their own division's slug; never ship the token to an untrusted client.

---

## What the overrides actually change

- `annual_calls` / `volunteer_count` → the numbers on the division **home** page (the stats band + count-up animation). `vehicle_count` → the count shown with the fleet preview.
- **Vehicle photos** → the image on the home **fleet-preview cards** and the **fleet page** vehicle cards.
- **Nothing else.** Other pages and the compiled config are untouched; if you never write an override, every page renders exactly as it does today (the render is byte-identical when no override is set).
- Borrowed / sister-division vehicles shown on a division's fleet keep their **owning** division's compiled photo — there's no per-image override for those here.

---

## Errors

Uniform envelope:
```
{ "error": { "code": "not_found", "message": "…" } }
```
Common codes: `unauthorized` (401), `insufficient_scope` (403), `not_found` (404 — unknown slug **or** vehicle code), `invalid_json` / `invalid_field` / `invalid_content_type` / `invalid_body` (400), `image_too_large` / `unsupported_type` (400), `empty_patch` (400), `method_not_allowed` (405), `no_database` / `no_media` (503).

---

## Gotchas

- **Consumer-enforced tenancy** — the token can edit any division; *your app* restricts each coordinator to their own slug. There is no per-division key.
- **`null` clears, omit keeps, `0` is a real value** — to reset a stat to the compiled default, send `null`; don't send `0`.
- **Vehicle `code` is case-sensitive** (`KJ-500`, not `kj-500`). Always source it from the merged view's `vehicles[].code`.
- **Render-time + cached** — overrides apply on the next render of the division's **home** and **fleet** pages. Those responses are Cloudflare-cached (`Vary: Host`), so a change may take a cache TTL / hard refresh to appear.
- **`shared` is not a division** (404). Only real division slugs have stats/fleet.
- **First-time setup** — the D1 tables (`division_overrides`, `division_fleet_images`; migration `0011`) must exist before the endpoints work. Until then the API returns errors, but the **public site is unaffected** — it falls back to the compiled config.
- Keep the token **server-side**; it's stored only as a hash and shown once at mint.
