# Session 0047.0 — Transcript > Date: 2026-05-30 > Started: 2026-05-30T11-02 · Ended: 2026-05-30T12-47 (PST implied) > Goal: Advance ROADMAP **#38** (Track Δ) — turn the session-0046 e2e > scaffold (rfc-app PR #4) into a working, green Tier-1 local-Docker suite. > No VM work (Track Δ's VM pieces stay gated/deferred to their own session). > > Outcome: **#38 Tier 1 is live and green.** `docker compose up --build` > stands up the full stack (frontend+backend+SQLite+Mailpit+real disposable > Gitea) and the Playwright suite passes 4/4, deterministically across > repeated runs: health, landing render, and the OTC sign-in vertical > (API round-trip + full browser UI). Pushed to **rfc-app PR #4** > (`session-0046/e2e-tier1-scaffold`, fast-forward `a3f48ba..15af891`). > One default-secure framework change rode along (`ratelimit.py` test > bypass). Not a release; no deploy; PR #4 left for the operator to merge. --- ## Pre-session state - **flotilla**: `main` @ `5f1689f` (v1.4.0). **v1 has shipped** — relevant below. - **rfc-app**: `main` @ `0c972c8` (v0.31.0). PR #4 open from session 0046: branch `session-0046/e2e-tier1-scaffold` @ `a3f48ba`, an `e2e/` scaffold (compose topology + helpers + `test.fixme` specs) — **not yet brought up or validated**; Dockerfiles + the Gitea bootstrap were the explicit TODO. - **OHM live**: v0.31.0 (deploys.id=46). Untouched this session. - **Other sessions in flight at claim time**: 0045 (`--INPROGRESS`, UX pass on the admin Users page — rfc-app *frontend*, disjoint from `e2e/`), plus stale `--INPROGRESS` placeholders 0026/0027. --- ## Turn-by-turn arc ### Arc 1 — Orient + the v1-gate question Read SESSION-PROTOCOL.md + the ROADMAP Track-Δ section (#37–#42). Claimed **session 0047** via `claim-session-id.sh` (placeholder pushed at `e76b1d3`). The launch prompt insisted on confirming **flotilla v1 timing** before any VM-side work. Asked the operator (Tier-1-only vs PPE-in-scope). The operator turned it back to me: *"can we do #2 this session or stick to #1?"* **My call: stick to Tier-1 (#1) this session.** Surfaced a factual correction — **flotilla v1 has already shipped (v1.4.0)**, so the "gated on v1" condition on Track Δ is *already satisfied*; #37 (PPE) isn't blocked by v1 timing, it's just a large, separate live-infra effort (new GCP project + micro VM + colocated `ppe.git` Gitea + DNS + a real flotilla multi-deployment code change). #38 Tier-1 is prerequisite-free, operator- gesture-free, and de-risks PPE (Tier 2 reuses the same suite). House discipline is one feature per session. ### Arc 2 — Map the backend (Explore subagent) Dispatched an Explore agent to produce a concrete reference: `config.py` required vars, boot/migration path (`app.main:app`, `db.run_migrations`), OTC endpoints (`POST /auth/otc/request` → 202; `POST /auth/otc/verify` → session cookie `rfc_session`), the Turnstile knob (`TURNSTILE_REQUIRED`), the `gitea.py` client surface, the seed scripts, the frontend build (`VITE_APP_NAME` required), `/api/health`, and the `deploy/nginx` proxy shape. Verified the load-bearing details directly afterward. **Key catch:** the `rfc_session` cookie is `Secure` by default → browsers drop it on `http://localhost` → sign-in wouldn't stick in Tier 1. Confirmed the opt-out is `SESSION_COOKIE_SECURE=false` (`main.py:152`). ### Arc 3 — Build the Tier-1 stack (in an isolated worktree) Per the parallel-session rule (0045 in flight, same repo), worked in a dedicated git worktree on `session-0047/e2e-tier1` (off the scaffold tip), never the shared checkout. Wrote: - `e2e/docker/backend.Dockerfile` (+ `backend-entrypoint.sh`) — repo-mirror layout so `parents[2]/VERSION` and the seed scripts' `parent.parent` resolve as in prod; entrypoint reads the minted bot token into `GITEA_BOT_TOKEN`. Same image runs the `gitea-seed` one-shot. - `e2e/docker/frontend.Dockerfile` + `nginx.conf` — Vite build → nginx serving the SPA + proxying `/api`,`/auth` to `backend:8000`. - `scripts/bootstrap_e2e_gitea.py` — create the org, then reuse `seed_meta_repo.py` to create + seed `META_REPO` (`ohm-content`). - Rewrote `docker-compose.yml`: `gitea` (real `gitea/gitea:1.22`), `gitea-init` (mint bot user+token via CLI → shared volume), `gitea-seed` (org + seed), `backend`, `frontend`, `mailpit`, chained by `depends_on`. - `.env.example`: added `SESSION_COOKIE_SECURE=false`, `GITEA_ADMIN_PASSWORD`, annotated the runtime-overridden `GITEA_BOT_TOKEN`; root `.dockerignore`. ### Arc 4 — Bring it up; three real obstacles, fixed Iterated against a live `docker compose up`: 1. **Gitea refuses to run as root** → every `gitea admin user create` FATAL'd. Fix: container is root only to `chown` the fresh token volume; the `gitea` commands drop to the `git` user via `su-exec`. 2. **gitea-init not idempotent** on an `up` over a persisted volume (user + token-name `e2e` already existed → exit 1). Fix: short-circuit if the token file exists; tolerate an existing user; unique token name. 3. **Per-IP OTC rate limiter** (`5 sends / 5 min`, `ratelimit.py`) — the suite drives all auth from one IP, so a normal run + CI retries trip it. This is a *real* determinism issue, not just test churn. Fix: a default-secure, env-gated `RFC_APP_RATELIMIT_DISABLED` bypass in `allow()` (mirrors `RFC_APP_INSECURE_WEBHOOKS`; durable per-email lockouts unaffected) + `tests/test_ratelimit_bypass.py` (incl. the default-still-enforces case, run green via a throwaway venv). After fixes: clean `down -v && up --build` → all services healthy, `/api/health` → `{"version":"0.31.0","status":"ok"}`, gitea seeded. ### Arc 5 — Green the suite Finalized the `test.fixme` specs against the live DOM: - `smoke.spec.js`: health shape + landing `