# coachpapa — v8 substrate feedback

Running log of friction hit while building this project inside the v8
conventions. **This is feedback about the substrate, not about coachpapa** —
it gets compiled into `/srv/.logs/planning/server-evolution-2026.md` (or the ops
queue, per that plan's scope rule) once the build settles.

Scope discipline: only entries where the *server framework, standards library,
or gateway* caused the friction. Bugs in this project's own code belong in
`handoff.md`, not here.

Each entry: what happened, what it cost, and a candidate fix.

---

## ✅ COMPILED UP — all 10 entries addressed 2026-08-04

This file did its job: every entry below has landed as a substrate change, so the
next project doesn't pay these costs again. Kept for provenance.

| # | Resolution |
|---|---|
| 1 | **Roadmap split.** T3.1 → **T3.1a** (workspace/instance shape, no gateway work — marked DONE) + **T3.1b** (instance-granular registry, still pending). The cheap half is no longer blocked behind the expensive half. |
| 2 | **Fixed in gateway.** `fix-permissions` now backfills setgid on every subdirectory (`_apply_setgid_to_subdirs`). Directories only — never a recursive chmod, which would widen mode-660 secret env files. Caught real drift on this project's `data/media/**` (container-created dirs). |
| 3 | **Documented.** `react.md` → "Secrets and env files under the gateway": the gateway must be able to read what compose needs (660 group `<project>-dev`, or 600 owned by `srv-gateway`), and never `${}`-interpolate a secret. `coding.md`'s 600 rule reworded as a floor with a pointer. |
| 4 | **Fixed in gateway.** `deploy --build` now uses `--progress=plain`, returns the last 120 lines of real output, and writes the full log to `.logs/deploy-failed.log` (0640). |
| 5 | **Fixed.** `react.md` status header now names hosting-web, `with`, and coachpapa as the production proofs. |
| 6 | **Documented.** The three-stage Dockerfile (python dumps `openapi.json` → node generates the TS client + builds → runtime discards node) is now the reference mechanism in `react.md`. |
| 7 | **Documented.** `react.md` ships the `extend-immutable-calls` ruff snippet for FastAPI's `Depends` idiom. |
| 8 | **Documented.** `react.md` now states the default (no published port, use `docker exec`) and when loopback exposure is justified. |
| 9 | **Fixed in gateway.** `_audit_deploy_state_tenant_apps()` extends the T1.4 check to tenant apps, comparing **image build time** (not container start — a plain `restart` would mask staleness) to newest source in the build context. Scoped to apps whose `build.context` is a dedicated instance subdir, which keeps it noise-free. It immediately flagged *this* project as built-but-not-deployed. |
| 10 | **Documented.** `coding.md`: "a staged placeholder must read as *absent*" — check the `REPLACE_WITH_` prefix, not just emptiness. |

Also promoted from this project into the standard: the **workspace scaffold**
(`/srv/projects/standards/new-workspace.sh`) and this feedback-file pattern itself,
both now part of the documented shape in `/srv/apps/CLAUDE.md`.

---

## 1. T3.1 confirmed at code level, and cheaper than the plan assumes

**Hit:** 2026-07-30, P0 setup.

`srv-gw deploy` hard-requires `docker-compose.yml` at the project root
(`gateway.py:3642`, refuses otherwise) and the tenant root is a single hardcoded
`APPS_DIR = Path("/srv/apps")` (`gateway.py:35`).

**Cost:** none, once understood. Putting code in `main/` and pointing the root
compose file's build context at `./main` works today with zero gateway changes.

**Candidate:** T3.1 stage (a) may be splittable. The *folder shape* half
(instance dir + build context) needs no gateway work at all and could be adopted
as a convention immediately, ahead of the instance-granular registry half. Worth
noting in the roadmap so the cheap part isn't blocked behind the expensive part.

---

## 2. `srv-gw fix-permissions` does not recurse setgid into new subdirectories

**Hit:** 2026-07-30, creating `main/`.

`fix-permissions` sets the project root to 2775 and applies default ACLs, but a
newly created subdirectory does not get the setgid bit. Worse, `chmod 2775` on
it **fails silently** when the session's supplementary groups don't include the
project group: the kernel strips S_ISGID, `chmod -v` reports success, and `stat`
still shows 775. Every file created underneath then lands with group `rian`
instead of `<project>-dev`, which the service user may not be able to read.

**Cost:** ~15 minutes of confusion, and it would have produced unreadable files
later. Workaround is `sg <project>-dev -c 'chmod 2775 <dir>'`.

**Candidate:** either (a) `fix-permissions` applies setgid recursively to
directories, or (b) the server docs carry the `sg` rule explicitly. Currently
`/srv/CLAUDE.md` documents `sg`/`newgrp` only for *write* failures, not for this
silent-strip case, which presents completely differently.

---

## 3. Secret-file permissions: an undocumented three-way tension

**Hit:** 2026-07-30, first deploy attempt (failed).

`coding.md` says compose env files should be "readable only by the service user
and rian," which reads as 600. But:
- the gateway (`srv-gateway`) must read the file to deploy, so 600/rian fails
  with `permission denied`;
- compose `${VAR}` interpolation reads only the shell and the gateway-managed
  `.env` — **not** `env_file` — so putting a secret in `env_file` and
  interpolating it silently yields a blank string with only a warning.

**Cost:** two failed deploys. The working pattern (which `with` already uses but
nothing documents): a separate `.app.env` at mode **660, group `<project>-dev`**,
consumed via `env_file` by every service that needs it, with **no `${}`
interpolation of secrets at all**.

**Candidate:** write this down — either in `react.md` (it's stack-adjacent) or a
gateway doc. It is not derivable and every new app on this stack will hit it.

---

## 4. `srv-gw deploy --build` truncates build output on failure

**Hit:** 2026-07-30, frontend build failure.

A failing Docker build returns only the last few lines plus the Dockerfile
fragment — enough to know *which* step failed, not *why*. I had to fall back to
raw `docker build --target frontend` to see the actual TypeScript errors.

**Cost:** one extra debug cycle for me. **For a developer it is a hard stop** —
they cannot run `docker` at all, so a failed build is undiagnosable through the
sanctioned tooling.

**Candidate:** return the last N lines of real build output (or write the full
log to a path the developer can read). This looks like the single highest-value
gateway DX fix surfaced by this build.

---

## 5. `react.md`: stale status header

**Hit:** 2026-07-30, planning.

Header says *"no app is built on this stack yet"*, but v8 §3.14 and §4 both name
hosting-web as the stack's production proof, and `with` runs on it. Surfaced in
the plan's deviation ledger rather than silently corrected (editing a standards
file as a side effect of another task violates surgical-changes).

**Candidate:** update the header; add coachpapa as a second data point.

---

## 6. `react.md`: the OpenAPI→TypeScript rule has no worked mechanism

**Hit:** 2026-07-30, P0.

The rule "generate the TS client from the backend's OpenAPI schema; never
hand-write a TS interface" is correct and it **caught a real defect here**. But
the doc doesn't say *how* to generate without a running server, which is the
whole difficulty in a container build.

**What worked** (now proven, offered as the pattern): a three-stage Dockerfile —
stage 1 (python) imports the FastAPI app and dumps `openapi.json`; stage 2 (node)
runs `openapi-typescript` against it, then builds the SPA; stage 3 is the runtime
with Node discarded. Fully reproducible, no running server, no committed
generated file drifting.

**Candidate:** fold that into `react.md` as the reference mechanism.

---

## 7. `react.md`: ruff and FastAPI's `Depends` idiom

**Hit:** 2026-07-30, first lint run.

Ruff's default bugbear rules flag **26 false-positive B008s** ("do not perform
function call in argument defaults") against FastAPI's core dependency-injection
idiom. The fix is config, not code:
`[lint.flake8-bugbear] extend-immutable-calls = ["fastapi.Depends", "fastapi.Query", ...]`.

**Candidate:** ship that snippet in `react.md`. Every app on this stack hits it,
and the tempting wrong fix is to suppress the rule or contort the code.

---

## 8. `react.md` vs `with`: Postgres host-port exposure

**Hit:** 2026-07-30, compose design.

`react.md` says the Postgres sidecar runs on the internal network "only — no
host port." `with` publishes `127.0.0.1:54317` for its diff harness and backups,
and documents why. Both are defensible; the doc reads as absolute.

**Candidate:** one sentence on when loopback exposure is justified (host-side
tooling that genuinely needs psql) versus the default of no published port at
all. coachpapa took the stricter reading and uses `docker exec`.

---

## 9. No built-vs-deployed drift detection for tenant apps

**Hit:** 2026-07-30, ongoing.

T1.4's deploy-state check covers `/srv/system` services only. For a tenant app
the only guard is the coding.md visible-version rule (`/api/meta` + UI footer),
which is manual and depends on someone looking.

**Candidate:** already implied by the roadmap ("later T1.4 extension"); this is a
second concrete request for it. A tenant app with a build step is exactly where
"built but not deployed" hides, because the build succeeding *looks* like a
deploy.

---

## 10. The `REPLACE_WITH_*` placeholder workflow has a truthiness trap

**Hit:** 2026-07-30, wiring Gemini transcription.

The server's mandated credential workflow stages secrets as
`KEY=REPLACE_WITH_KEY` for a human to fill in their own shell. That is the right
workflow — but a placeholder is a **non-empty string**, so the natural check
(`if not settings.api_key`) reports the feature as *configured*. The UI then
offers a control that fails at first use, instead of degrading cleanly.

Caught here only because the app exposes a `/status` endpoint and I looked at it;
it would otherwise have shipped as "the record button is broken."

**Cost:** one deploy cycle. Fixed with a `has_<x>_key` property that rejects any
value starting with `REPLACE_WITH_`, plus tests.

**Candidate:** put the rule in `coding.md` next to the existing placeholder
workflow — *"a staged placeholder must read as absent; check for the placeholder
prefix, not just emptiness."* It is a one-line rule that every app following the
documented workflow needs, and the failure is silent without it.
