# Settings page is dead — the collector never implements `/config/projects`

audience: AI coding agents first. Request spec for a factory run. Contract-level: seams and
behavior are pinned; write the bodies yourself by reading the repo.

**Repo root:** `/home/user/Projects/overdeck/.worktrees/factory-settings`. Every path is
relative to it.

## The defect (reproduced, not guessed)

The `/settings` page renders `Something went wrong — collector request failed:
/api/collector/config/projects → HTTP 404`. The whole page is unusable.

Root cause, verified in three places:

1. `apps/web/src/lib/collector-client.ts:241` — `fetchProjectColors()` GETs
   `/api/collector/config/projects`; `:245` `saveProjectColors()` POSTs the same path with
   body `{projects}`.
2. `apps/web/src/pages/api/collector/[...path].ts` — the proxy allowlists `config/projects`
   for reads (`PROXYABLE_READ_PATHS`) and has a `CONFIG_PROJECTS_PATH` mutation handler, so
   it forwards the request to the collector unchanged.
3. `collector/src/server.ts` — there is **no** `url.pathname === "/config/projects"` branch.
   Every route is an explicit `if (url.pathname === ...)` comparison; this one was never
   written. Confirmed absent in the deployed clone as well
   (`/home/user/.local/share/overdeck/deploy/collector/src/server.ts`).

The data the route is supposed to serve already exists: `collector/src/config.ts:79` defines
`projectColors: z.record(z.string()).default({})`.

So: the client and the proxy were built; the collector half never was. Implement it.

## Contract

Add two routes to `collector/src/server.ts`, following the exact shape of the surrounding
route branches (same auth handling, same `Response.json`, same error style — read the
neighbouring `/hooks` and `/incidents` branches and match them; do not invent a new pattern).

```
GET  /config/projects  -> 200 {projects: Record<string,string>}
POST /config/projects  <- {projects: Record<string,string>}  -> 200 {projects: <persisted>}
```

- `GET` returns the current `projectColors` from the collector's config. Never 404, never
  500 on a missing/empty key — an unset map is `{}`, which is a valid answer.
- `POST` validates the body with the same zod shape already used for `projectColors`
  (`z.record(z.string())`), persists it to the collector's config file so it survives a
  restart, updates the in-memory config the running process serves, and returns the
  persisted map. Invalid body → 400 with a named error, never a silent partial write.
- Persisting MUST rewrite only the `projectColors` key. Never rewrite the whole config from
  an in-memory object that could drop keys another writer added — read, merge, write.
- Write atomically (temp file + rename) so a crash mid-write cannot leave an unparseable
  config that bricks the collector on next start.
- Colour values are attacker-reachable strings from a POST body: validate each value is a
  plain CSS colour token (`#rgb`/`#rrggbb`, or a bare CSS named colour) and reject anything
  else with a 400. Do not store arbitrary strings that later land in a `style` attribute.

Do **not** widen `PROXYABLE_READ_PATHS` or add proxy routes — the proxy already handles this
path in both directions. If you find yourself editing
`apps/web/src/pages/api/collector/[...path].ts`, stop: the bug is not there.

## Acceptance — run every one of these and report the real output

1. `bun test` in `collector/` — green, including a new test covering: GET returns `{projects}`
   with the configured map; POST persists and the value survives a fresh config read; an
   invalid colour value is rejected with 400; POST does not drop unrelated config keys.
2. `pnpm --filter web typecheck` — green.
3. Live proof against the running collector, both verbs, reporting the actual HTTP status
   and body. The collector is a systemd user unit (`overdeck-collector.service`) listening on
   `127.0.0.1:8787` and it requires the auth token — read how the web proxy obtains that
   token in `apps/web/src/pages/api/collector/[...path].ts` and use the same source. A `401`
   is not a pass; neither is a `404`.
4. State plainly whether `/settings` still shows the error banner, and what you observed.

## Hard prohibitions

- Work only inside the repo root named above.
- Never weaken auth on a collector route to make a check pass.
- Never edit anything under `/home/user/.local/share/overdeck/deploy/` — that is the deploy
  clone, and it is not yours to touch.
- UI rules are MANDATORY if you touch `apps/web/src/**`: read `.claude/skills/od-ui-dev/SKILL.md`
  first, compose existing exports, tokens only, and NEVER create a new primitive.
