# Overnight build log — 2026-07-19

A running log of the autonomous overnight build, newest section at the bottom.
Read this first in the morning, then `docs/plan-v1.md` for the full design.

## The ONE human step waiting for you

BW Auth requires a real interactive login that Claude can't do while you sleep
(it never handles your password). Everything is built and tested against a
synthesized identity; the single live check is: over LAN/Tailscale, visit
`https://volleyboard.bowden.works`, complete the `auth.bowden.works` login, and
confirm the board loads. Details + exact commands land in this doc as M0 closes.

## Stack (as decided)

- FastAPI (one Python process) + Vite React SPA + Postgres 17 sidecar, all on
  mosiah. No Supabase. BW Auth Pattern B (OAuth2/PKCE) mirroring `/srv/apps/with`.
- Deployed via `srv-gw deploy --project volleyboard --build`; host stays
  LAN-only until you've verified login (above), then flip public.

## Progress

### Setup + planning (done before build)
- Infra: volleyboard.bowden.works live, gated, 5 players granted.
- Plan written + critiqued 3x: `docs/plan-v1.md`. Review learnings +
  patterns reference: `docs/review-learnings.md`, `docs/review-patterns-ref.md`.

### M0 — walking skeleton (in progress)
- [x] Backend foundation: models (8 core tables), config, db session, Alembic
      0001 — schema validated against a throwaway Postgres, `alembic check`
      clean, functional insert chain confirmed (caught + fixed a `position`
      NOT NULL default bug).
- [x] In-process SSE event bus + `/api/events`.
- [x] docker-compose (app + pg sidecar, healthcheck-gated, internal network),
      multi-stage Dockerfile, entrypoint (migrate-then-serve), `.env` with a
      generated DB password (never printed).
- [x] Frontend M0 scaffold (Vite+React19+TS+Tailwind v4, TanStack Query),
      `npm ci && npm run build` verified — shows version + live SSE heartbeat.
- [x] BW Auth (OAuth2/PKCE) + View As (Fable) — mirrors `/srv/apps/with`,
      **59/59 tests pass** (token verifier matrix, full PKCE callback flow,
      allowlist, production boot-guard, View-As matrix, column-scoped
      provisioning). Headers never grant identity (verified). Dev-bypass stub
      for overnight; real login refuses to be spoofed.
- [x] **M0 integrated + verified locally**: booted the full app (dev-bypass,
      dev DB), all endpoints green (/health, /api/version=0.1.0, /api/me with
      View-As targets, SSE `hello` stream), SPA renders "Volleyboard" + live
      "Connected" heartbeat + version footer (screenshot confirmed). Spoofed
      `X-Auth-User` correctly ignored.
- [ ] Gateway deploy — batched to end of night (build M1/M2 locally first,
      deploy once). Register/un-gate is owner-present-preferred; commands
      staged. The one human BW login waits for morning.

### M0 verification recipe (for reuse)
- Local run: `cd backend && APP_ENV=dev AUTH_DEV_USER=rian
  DATABASE_URL=postgresql+psycopg2://volleyboard:volleyboard@127.0.0.1:5433/volleyboard
  STATIC_DIR=$PWD/../frontend/dist BW_CLIENT_ID=voll-dev BW_CLIENT_SECRET=dev
  .venv/bin/uvicorn app.main:app --port 8000`
- Screenshot: `google-chrome --headless --disable-gpu --no-sandbox
  --disable-dev-shm-usage --screenshot=out.png --window-size=1400,900 URL`
- `X-View-As: <username>` header switches rendered perspective (dev only).

### Bonus (built ahead of sequence while auth compiled)
- [x] **Perspective engine** (`backend/app/services/perspective.py`) — the M6
      soul logic (`rep`/`courts_of`/`visible_players`/touch-visibility) written
      and VALIDATED: 19/19 assertions from the fleshout worked example pass
      (Zeina collapses to Adi for Rian; Erin invisible to Zeina; sub-team
      chatter hidden; boundary-lead collapse; side-label bottom-out). The
      hardest, most novel part of the app is proven before it's wired — M6 is
      now "project it through the API," not "invent it."

### M1 — the board (in progress)
- [~] Backend (board API + real Brentwood seed transform: 218 pages / 30
      features / 11 issues → courts+balls; /api/meta vocab; PATCH inline edits
      w/ status↔review coupling) — building (Fable).
- [ ] Frontend board UI (dispatched after backend lands, so it builds + is
      screenshot-verified against the real API + real data in one pass).

### M1 backend — DONE (Fable)
- [x] `/api/meta` (single vocab+color authority, exact review palette),
      board read, PATCH w/ bidirectional status↔review coupling, POST serve.
      Real seed loaded: **259 balls** (218 pages / 30 features / 11 issues),
      full fleshout team tree (Tingang + Back office). 73/73 tests at the
      time; idempotent loader (`tools/load-seed.py`).

### M2 backend — DONE (main session, built ahead while frontend runs)
- [x] The rally engine (`services/touches.py` + `routers/rally.py`):
      append-only touches; serve-on-create; explicit pass endpoint;
      status automations as MOVEMENT (Ready for Review = auto-spike to
      author + rally memory; Revision Required = restore; Resolved = clear
      hands + stamp + ACE detection for one-touch put-aways; reopen
      restores; Informational -> FYI Only announcements); every change
      publishes SSE (M3 pre-fed). PATCH response carries `ace` for the
      client flourish. **82/82 tests green** (two M1-exclusion tests
      updated to assert the M2 behavior they excluded).

### M4 backend — DONE (subagent)
- [x] Comments (polymorphic parent, acting_node_id visibility scope from
      day one, author-gated edit/delete, author backdating) + attachments
      (upload w/ mime+size caps + traversal guards, authed serving),
      mention parsing, SSE events. Migration 0002.

### M5 backend — DONE (subagent + main-session follow-up)
- [x] Notifications engine, review design relocated to services (three
      kinds, mention-beats-thread_reply dedup, participation=subscription,
      context_label, 160-char stripped preview, resolved propagation via
      ball_id, per-recipient SSE). Bell API (list w/ true unread/unresolved
      counts, read, read-all; foreign ids 404 never 403). Migration 0003.
- [x] Follow-up fix (main session): the SERVE now notifies initial
      assignees too (the review INSERT-trigger case the hook list missed).
      **117/117 tests green.**

### M1 frontend — DONE (Fable) + verified by orchestrator screenshots
- [x] The whole board UI: 3 courts on real Brentwood data, exact review
      color language via /api/meta (single authority; generated TS API
      types), StatChips w/ real counts, filters/search/sort/tags, "Mine"
      default on Issues, side sheet w/ deep links, serve sheet w/ Cmd+Enter,
      inline edit verified END-TO-END through the real UI (status pill
      click -> PATCH -> coupling reconciled -> chips recount). Phone-width
      cards clean; dark mode works. Zero console errors.
- [x] Learning: headless Chrome --screenshot can't load http on this box;
      the working CDP driver is scratchpad/shot.py (reused by later
      agents).
- Backend seams the frontend flagged, closed by main session same hour:
  meta.done_statuses + non_actionable; board payload last_touch_at (aging
  from latest touch). 117/117 after.

### Stall + recovery (~03:00-09:30)
- Permission prompts default-denied while rian's screen hibernated; all
  agents stopped cleanly mid-M2-verification / mid-M6-wiring. Rian
  confirmed no intentional denial; resumed ~09:30. The one dangling edit
  (_validate_perspective_writes call site in services/board.py) was
  completed by the main session; suite back to 117/117 before resuming
  agents.

### M2 frontend — DONE (Fable), verified with driven screenshots + audio
      instrumentation
- [x] sounds.ts (5 wavs, action-gated — audited: zero play() reachable
      from load/SSE), beats.ts (resolve hold->glide w/ own:false seam),
      motion-layer.tsx (WAAPI arc + drop, reduced-motion safe),
      court-strip.tsx (sticky band, count bubbles, net line), aging wobble
      off server last_touch_at (+ ?demo-aging=1), "In play" chip rename,
      Resolved eye-toggle w/ reveal-on-explicit-ask, SSE board freshness.
      Driven-and-asserted: pass mid-arc, ACE beat at 1s, glide-out by
      3.5s, serve landing, wobble, M1 inline-edit intact.

### M6 backend — DONE (Fable): 175/175 at handoff
- [x] Perspective wired through EVERY surface: board (sides/strip bands/
      scoped roster/rep'd assignments/holder_label/can_spike), rally
      collapse, comment scoping, notification actor rep + write-side
      scoping (a Tingang preview can never reach Rian's bell),
      per-subscriber SSE projection (inner passes dropped; notifications
      recipient-only), mentionable endpoint, write-side rules (422
      TARGET_NOT_VISIBLE / AUTOMATION_TARGET_NOT_VISIBLE), autopass
      (0004, one-hop, owner API) + adi's from_subteam->rian rule, M6 seeds
      (4 Tingang rallies, Rob court, scoped comments, 3 curated aged
      balls), §4.2 as 49 tests + 9 autopass tests.
- Judgment call (accepted): §4.2 functions win over the §4.3 prose example
  — a sub-team viewer's far label is always their OWN root side's name.

### M3 backend + M7 backend bits — DONE (main session)
- [x] Presence on the SSE bus: connect/disconnect w/ 10s grace, online
      set in the hello frame + presence.updated events (ids-only transit;
      strip renders only projected avatars — per-subscriber presence
      filtering is a noted M7+ tightening). 3 tests.
- [x] View As read-only ENFORCED at the middleware (403
      VIEW_AS_READ_ONLY on mutating /api/* while impersonating;
      view-as/auth paths exempt) — closes the M6 agent's flagged gap.
- [x] Warm-up court live: 4th court config + 6 hand-authored pre-game
      balls (3 resolved, FYI announcement, 2 open requests). Seed
      idempotent. **179/179.**

### M4+M5 frontend — DONE (Fable): composer (paste-screenshots, upward
      mention autocomplete, interactive checkboxes), rally timeline as
      play-by-play (collapsed entries rendered), the bell (exact review
      badge logic, deep links w/ _t buster) — all driven end-to-end incl.
      live SSE comment + bell updates.

### M3+M6+M7 frontend — DONE (Fable, survived a login-expiry interruption):
      live layer (presence glow, cross-client staggered motion, arrival
      sound gate, foreign resolves dim in place), the soul made visible
      (server strip bands + multi-band stacking, holder_label chips,
      can_spike omission, autopass sheet, View As UI + read-only banner),
      polish (3-sentence how-to-play, empty states). **View As walkthrough:
      26/26 §4.2 assertions PASS** (walkthrough-{rian,erin,adi,zeina,rob}
      .png). A REAL UI-driven autopass chain verified (zeina->adi auto->
      rian, bolt-marked).

### DEPLOYED — v0.7.0 live (2026-07-19 ~11:30)
- [x] 179/179 backend tests; version 0.7.0 + CHANGELOG.md.
- [x] Deploy: lan-only shield ON -> srv-gw deploy --build (gotcha: .env
      must be 0640 for the gateway) -> app-client-register --secret-out
      (secret never in chat; file 0600 srv-gateway) -> RECREATE (not
      restart — env_file reads at create) -> in-container seed (265 balls)
      -> full OAuth chain verified: site -> /auth/login -> auth.bowden.works
      /app-authorize?client_id=volleyboard. API default-deny 401 confirmed.
- [x] Security audit: 0 criticals; orphaned devdb container removed.
- **Site remains LAN-only** until rian verifies a live BW login, then:
  `srv-gw id-site-set --host volleyboard.bowden.works --lan-only false`

### In flight (parallel Fable agents)
- [~] M2 frontend — THE FEEL: sounds module (own-actions only, mute,
      no-sound-on-load), FLIP chip flight, serve moment, resolve beat w/
      2.5s hold + glide (own-resolve only), ace flourish, aging wobble,
      THE COURT STRIP (one sticky band, count bubbles, net line), StatChip
      "In play" rename, SSE board invalidation.
- [~] M6 backend — THE SOUL: rep()/courts_of through every API surface
      (board strip bands + holder_label, rally collapse, comment scoping,
      per-subscriber SSE projection + recipient-only notifications,
      mentionable), write-side rules (pass target validation, can_spike),
      autopass (model 0004 + one-hop execution + API), Tingang/Rob seed
      rallies + curated aging demo, §4.2 as a pytest suite.

### M2 prep (done ahead)
- [x] Five synthesized sound assets (`frontend/public/sounds/{serve,pass,
      spike,resolve,ace}.wav`) — pure-Python synth, bundled locally, served at
      `/sounds/`. Placeholders to tune in M7; the sensory scaffold is ready.

_(more appended as milestones land)_
