# coachpapa — handoff

> Append-only, newest-first. One entry per session that meaningfully advances
> or changes the project. Cold sessions orient from here + `brief.md` alone.
> Format per entry: `## YYYY-MM-DD — <one-line summary>` then a short body.

## 2026-07-30 — First-run walkthrough for Mike (v0.6.1)

**What it is.** A six-step checklist covering Mike's own flow chart, which ticks
a step only when the action **actually happens** — a transcript returns, a
ticket is created, a part is recorded, a ticket reaches closed, a CSV
downloads. Deliberately not a click-through tour: finishing it means he really
exercised the loop, and a step he never completed tells us where he got stuck.

**Why it also matters for feedback.** It ends with one narrow question — "what
is the ONE thing most wrong with it?" — which is the same anti-cascade strategy
as the rest of the project. An open "what do you think?" produces a week of
email.

**Design constraints honoured.** Opens on first visit, then collapses to a slim
bottom bar so it never undoes the phone-first layout work; dismissible to a
small "Show guide" pill; resettable. Progress lives in localStorage, not the
database — a guided tour has no business in an auditable maintenance record.

**Defect found and fixed.** Step one ticked only when the technician was
*changed*, stranding anyone who already had a name selected. It now reflects
actual state rather than a transition. Same class of bug as the reactive-actor
one from v0.5.1: deriving from an event instead of from state.

**Process note.** v0.6.1's fix initially shipped under the 0.6.0 label — a
`sed` bumped VERSION from the wrong working directory and failed silently while
the deploy succeeded. Caught by checking `/api/meta` against the file. This is
exactly the failure the visible-version rule exists to catch, and the reason it
is worth checking rather than assuming.

## 2026-07-30 — Demo-complete: planner loop, exports, asset auto-match (v0.5.1)

**The prototype now covers Mike's flow chart end to end.** Both gates green,
30 unit tests, seeded with a realistic 15-ticket history.

**Landed.**
- **Exports in the UI** (planner only). They are authorised routes, so a plain
  `<a download>` would arrive without the actor header and be refused - the
  client fetches with headers and hands the browser a blob.
- **Assignment + scheduling.** New `tickets.scheduled_for` (migration 0003).
  Both write audit events.
- **Role-aware views.** Planners triage/schedule/assign; technicians work and
  close. Everyone sees the whole history. Split taken straight from the flow
  chart.
- **Parts picker shows stock on hand + location** — his "confirm parts
  available" step. Uses the real seeded quantities.
- **Asset auto-match from speech** (`services/assets.py`). His example opens
  with the equipment name, so the asset is usually already in the sentence.
  Deterministic token overlap, no AI - instant, free, explainable. Applied only
  as a pre-selection the technician can override, and only when nothing is
  chosen yet. Voice-to-PART-number matching remains out of scope.
- **`scripts/seed_demo.py`** — 15 tickets across real Britco assets with varied
  statuses, assignments, schedules and parts usage. **Every generated ticket
  carries a `demo_seed` audit event**, so fabricated activity is identifiable
  from inside the record rather than by a naming convention. There is
  deliberately no purge command: tickets are never deleted, and adding a delete
  path to tidy demo data would break the guarantee gate-p0 asserts.

**Two defects found by testing, both mine.**
1. The asset matcher checked the equipment number *after* the "no word overlap
   -> skip" guard, so an asset named only by its number ("problem on
   2KL-001-03.1") could never match. Caught by a unit test written from real
   equipment numbers.
2. Switching technician did not update role-aware UI — `useActor` read
   localStorage during render, which is not reactive, so planner controls and
   export buttons kept the previous person's permissions until a page reload.
   Found by actually clicking through as two different people. Fixed with a
   subscribable store consumed via `useSyncExternalStore`.

**Deliberately still not done:** Pattern B auth (rian's call - the name picker
is less friction for a demo than making Mike log in), offline capture queue and
barcode scanning (both P3).

## 2026-07-30 — Phone-first capture redesign (v0.4.0)

Transcription confirmed working against the real Gemini key by rian.

**Redesign.** The capture form now fits one phone screen without scrolling
(~520px vs ~900px). Record is the hero action — new `xl` Button size, primary
variant — and the description is no longer a tall text box: the transcript
renders as a compact tap-to-edit card, with "or type it instead" as the
fallback when nothing has been spoken. Photo collapsed to a compact row,
masthead/tabs/footer tightened, page title moved to `.visually-hidden`.

Rationale (rian): speaking is the premise, so the text box was competing with
the thing it is supposed to be a fallback for.

**Note on method.** The `xl` size was added to `Button` rather than wrapping the
button to patch its height — `frontend.md` forbids wrapper-restyling; a
documented variant is the sanctioned route.

## 2026-07-30 — Voice memos with transcription (v0.3.0)

**Scope change, recorded.** Server-side transcription was a locked "later phase"
decision in plan §2; rian revised it — the prototype is not demonstrable to Mike
without speaking, since that is the whole premise. Plan §2 and the out-of-scope
list are both updated with the rationale. Voice-to-*part-number* matching remains
out of scope; this is plain speech-to-text.

**Built.** Record in-app (MediaRecorder) -> POST `/api/transcribe` -> the
transcript fills the description, editable -> the recording itself rides along as
an attachment on the ticket. Modelled on garden2's implementation
(`/srv/apps/garden2/backend/app/services/ai/transcribe.py`) — same stdlib urllib
approach and inline base64 — but behind a **provider seam** (`services/ai/
providers/`) so swapping model or vendor is one file plus one registry line, per
`react.md`. `CP_TRANSCRIPTION_PROVIDER` and `CP_TRANSCRIBE_MODEL` select.

**Failure posture.** Transcription is soft everywhere: the ticket is saved before
attachments are processed, a failed transcription still leaves the audio attached,
and `/api/transcribe/status` lets the UI hide the recorder when no provider is
configured rather than offering a button that fails.

**ACTION REQUIRED — the key is a placeholder.** `.app.env` carries
`CP_GEMINI_API_KEY=REPLACE_WITH_GEMINI_API_KEY`. Transcription is inert until
rian fills it in his own shell (the same key garden2 uses lives in
`/srv/apps/garden2/.env`). After filling: `srv-gw deploy --project coachpapa`
(no `--build` needed — env only). Verify with
`curl -s -H 'X-CP-Actor: 3' http://172.17.0.1:3140/api/transcribe/status`
returning `{"configured":true}`. **Never cat the file to check.**

**Defect worth knowing.** First deploy reported `configured:true` on the
placeholder — a non-empty string is truthy. The record button would have appeared
and then failed. Fixed with a `has_gemini_key` property that rejects
`REPLACE_WITH_*`, plus tests. Logged as v8-feedback #10, because it is a hazard
of the server's own documented credential workflow, not of this app.

## 2026-07-30 — P0 and P1 built, deployed and gated; P2 partial

**State: a working prototype is live at https://coachpapa.demoing.info (v0.2.0),
behind the id-auth shared-password gate.** Both gate scripts pass — 28 assertions
total. Verified end to end in a real browser, not just by curl.

**Built.** FastAPI + React 19 SPA + Postgres 17 sidecar in one container, code in
`main/`, per `react.md`. Capture screen (asset search, dictation-friendly text,
rear-camera photo, priority), ticket list, ticket detail with status transitions,
parts used/returned, notes, append-only audit trail, follow-up spawning, and both
CSV exports. Seeded with Mike's real data: 376 Britco assets, 462 parts.

**Gates** (`main/scripts/gate-p0.sh`, `gate-p1.sh`) run against the live
deployment: numbering durability across restart, delete refusal, media
authorisation, and a full flow-chart replay with audit-trail assertions. They
void their own tickets afterwards — numbers stay consumed, which is correct.

**Four real defects the process caught, worth knowing about:**
1. The generated TS client refused to compile because `media`/`events`/
   `parts_used` were optional in the OpenAPI schema (Pydantic `default_factory`)
   while the service always populates them. Fixed backend-side by making them
   required. This is exactly the cross-boundary check `react.md` promises.
2. Alembic created the enum types twice (explicit `.create()` plus `create_table`
   doing it implicitly). Fixed with `create_type=False` and idempotent DDL.
3. Mike's `BP Parts Report.xlsx` contains mojibake (UTF-8 decoded as cp1252) —
   verified as a defect in HIS source file, not something we introduced. Repaired
   at extraction. The first repair only fixed 1 of 13 values because a
   whole-string cp1252 encode raises on C1 codepoints; a fixture test caught it
   and the per-character fallback fixed the rest.
4. The P0 gate flagged media as reachable statically. Investigated rather than
   trusted: it was the SPA catch-all returning the HTML shell (786 b), not the
   image. The assertion was naive, so it now checks content type. Hardened the
   catch-all with a containment check anyway.

**Security.** `srv-gw security-audit` run after deploy: coachpapa contributes
INFO entries only, no new warnings. Postgres has no published port at all
(internal compose network); media is served only through an authorised route and
is not reachable as a static file (asserted); the DB secret was machine-generated
straight into a 660 file and never printed. The one CRITICAL in the audit is
pre-existing and unrelated (`brentwooddev` disk usage).

**Not done, in priority order.**
- **Pattern B auth.** Still the name picker. `app/services/identity.py` is the
  single swap point. Do NOT register the app-client until the app draws its own
  BW login — registration auto-ungates the host.
- **Assignment UI.** The API takes `assigned_to_id`; nothing calls it.
- **`main/scripts/backup-db.sh` is written and tested but NOT scheduled.** It
  needs a line in rian's crontab (the script's header has it). Until then there
  is no automatic dump at all, and even once scheduled it is same-disk.
- P3 items (offline queue, barcode scanning) remain deliberately out of scope.

**Substrate feedback is now being logged.** `.logs/v8-feedback.md` collects
friction caused by the server framework/standards/gateway (as distinct from bugs
in this project). Nine entries backfilled from this build. It compiles into
`/srv/.logs/planning/server-evolution-2026.md` or the ops queue once the build
settles — do not edit the server plan directly from here.

**Next session.** Either wire Pattern B (see plan §6) or put the prototype in
front of Mike first — the plan's position is that the demo is a better question
than an email. If demoing first: he logs in with the shared password, picks a
name, and reports a problem.

## 2026-07-30 — Requirements extracted from Mike's documents; project planned to P2

**What happened.** Read the whole of `notes/` (3 Word docs, 4 videos sampled as
frames, the spreadsheets, two `.msg` emails, the plant photos) and reduced it to a
buildable scope. `COACHPAPA FLOW CHART.docx` is the spec of record; everything
else is context. Then planned the project against the v8 architecture.

**Landed.**
- `brief.md` — filled in: what/why/who, the stack decision, a reading guide to
  the six files in `notes/` that actually matter, and two known gaps (no backup
  substrate; no confirmed live customer).
- `.logs/planning/coachpapa-prototype-2026.md` — the build plan: locked
  decisions, the v8 deviation ledger, data model derived from Mike's real
  spreadsheets, milestones P0-P2 with script gates, security posture, and a
  binding out-of-scope list.
- `agents.md` — five project-specific rules earned their place (name-picker is
  not auth; don't register the BW client early; code in `main/` and state at the
  workspace root; media never static-served; the out-of-scope list is binding).
- `main/` created, empty but for its role header.

**Decisions worth knowing.** Stack is FastAPI + React 19 + Postgres per
`react.md` — it is the only fit in the library, and Mike independently asked for
a PWA in his own exec summary, so no App Store and no developer fee. Identity is
a name picker through P1 and Pattern B from P2, contained behind
`app/services/identity.py` so the swap is one file. The `main/` instance shape
costs nothing today: the compose file stays at the workspace root and builds from
`./main`.

**Gotcha found and recorded.** Creating `main/` exposed a silent-failure mode:
this session's supplementary groups don't include `coachpapa-dev`, so the kernel
stripped the setgid bit on `chmod 2775` without an error — `chmod -v` reported
success while `stat` showed `775`, and files created underneath landed with group
`rian`. Fixed with `sg coachpapa-dev -c 'chmod 2775 main'`; verified by creating a
throwaway file that now inherits `coachpapa-dev`. Rule added to `agents.md`.
`srv-gw fix-permissions` does not recurse setgid into new subdirectories.

**Surfaced, not resolved.** `react.md`'s header claims no app is built on the
stack yet, but v8 §3.14/§4 name hosting-web as its production proof and `with`
runs on it. The status line is stale. Left alone deliberately — editing a
standards file as a side effect of this task would violate surgical-changes.

**Next session.** P0, starting with the `main/` skeleton: Dockerfile (node build
stage into a python runtime), `app/{routers,services,models}`, alembic init, the
compose rewrite (app + Postgres sidecar, replacing the nginx scaffold), and
`.app.env` staged with placeholders for rian to fill. Then migration 001 and the
seed importers that read the real `.xlsx` files out of `notes/`. Plan §7 has the
full P0 list and its gate.

**Not needed from Mike.** Nothing, deliberately. The first and only ask comes
after P2: one link, one question, one answer.
