# Sent in

The community camera's public roll: photographs readers took with the
chch.live app while they were out in Christchurch. Each one is looked at by
a human before it lands; a report on any card takes it back off the wall
until a human re-decides.

A different provenance from `/wall`, those photos are the council's report
stream, these are the city's readers'. Cards say **Sent in**, never
"reported".

## Human page

`/sent-in`: newest first, tap a card for the full photo, the suburb pick
scopes the roll (`/sent-in?suburb=<slug>`).

## For the app (the API contract)

Reads are public JSON under `api.chch.live/cam/` (the site reaches them
same-origin through `/api/cam/`):

- `GET /cam/feed?limit=<n>&before=<key>&suburb=<slug>`, the approved roll,
  newest first, keyset-paginated. Items carry `id`, `caption`, `suburb`
  (slug, when the pin or the picker landed in one), `taken`, `published`,
  `w`, `h`.
- `GET /cam/img?id=<id>&w=<160|320|480|768|1200>`, one approved photo at a
  ladder width. Only approved ids resolve, a pending, reported or rejected
  upload never leaves the queue.
- `POST /cam/report`, `{"id": "<photo id>", "reason": "…"}`: takes the
  photo off the wall at once and drops it back on the review desk. This is
  the takedown path every card exposes.
- `GET /cam/health`: liveness plus queue counts.

Uploading is a two-step write, the app's lane, built on camera capture
(`capture` input / `getUserMedia`):

1. `POST /cam/begin` → `{"token": "…", "upload": "/cam/upload",
   "expires": <ms>}`. The token is one-shot, fifteen minutes, per upload –
   it is the auth-lite surface, so treat it as a credential (header
   `x-cam-token`, never in the query string).
2. `POST /cam/upload` once per photo: header `x-cam-token`, content-type
   `image/*`, the photo's raw bytes as the body. Optional query params:
   `caption`, `lat`, `lon`, `taken` (epoch ms). The server re-reads EXIF
   for `taken`/GPS when the params are absent; a pin inside the
   Christchurch box resolves the suburb itself. Returns the photo id and
   its queue state, `pending` until a human decides.

Limits: JPEG/PNG/WebP/HEIF in, ≤ 15 MB a photo, one token per photo, 30 a
day per address. Everything
is normalised server-side (EXIF orientation applied, metadata stripped,
re-encoded); GPS never appears on the public card beyond the suburb. Google
Vision SafeSearch scores the queue as an assist, the human is the decision;
a `VERY_LIKELY` adult/violence score rejects on its own but stays listed on
the desk, reversible like any verdict.

Errors are `{"error": "<plain words>"}` with a matching HTTP status –
401 no/expired token, 413 over 15 MB, 415 not a readable photo, 403 a
photo already refused, 429 the day's caps, 503 the queue is full, 404 the
id never was.

## Moderation (the desk's side)

Nothing is public until approved. The review desk runs loopback-only on the
VPS (`127.0.0.1:3018`, reached by SSH port-forward); it lists the queue with
its SafeSearch flags and takes `approve` / `reject` / `nudge place`
decisions, each stamped with the reviewer's name. A public report hides the
photo, bumps it back to `pending`, and notifies nobody automatically, the
re-decision is human.
