diff --git a/docs/design/apt-air-editor.md b/docs/design/apt-air-editor.md index f8ed688..fb1d658 100644 --- a/docs/design/apt-air-editor.md +++ b/docs/design/apt-air-editor.md @@ -5,17 +5,35 @@ | **Document** | Integrated .APT + .AIR Editor on openfsd Web UI | | **Author** | _(design author / implementer)_ | | **Date** | 2026-07-23 | -| **Status** | Draft (rev 2 — review issues addressed) | +| **Status** | **Implemented** (see Implementation status) | | **Target repo** | `/Users/rnorris/scratch/openfsd` | -| **Related** | `docs/design/sweatbox-integrated-simulator.md`, `internal/sweatbox`, `internal/web` | +| **Related** | `docs/design/sweatbox-integrated-simulator.md`, `pkg/twrfiles`, `internal/sweatbox`, `internal/web` | + +--- + +## Implementation status + +Shipped. Summary of the tree as of closeout: + +| Area | Location / notes | +|------|------------------| +| Parse + Format | `pkg/twrfiles` (`ParseAPT`/`ParseAIR`, `FormatAPT`/`FormatAIR`, fixtures under `pkg/twrfiles/testdata`) | +| Sweatbox | Thin aliases/wrappers over `pkg/twrfiles` (engine still pure) | +| Editor MPA | `/airport-editor` shell + Leaflet PE; echo-download; Admin-only | +| Validate API | `POST /api/v1/editor/validate-apt` / `validate-air` via `pkg/twrfiles` | +| Web JS | Pure modules under `internal/web/static/js/openfsd/`; Node tests in `webjs/` + `scripts/check-webjs.sh` in CI | +| Persistence | **None** durable (no disk/DB); Blob download + transient request bodies only | +| Playwright | **Cancelled** — no browser automation suite | + +Design history below is retained. Stale “Format missing / JS tests none” rows are updated in Background. --- ## Overview -openfsd already loads TWRTrainer-compatible **`.apt`** (airport geometry) and **`.air`** (scenario aircraft snapshots) into an in-process sweatbox simulator and exposes an instructor control panel at `/sweatbox`. Instructors still author those files outside the product — typically in a text editor or the legacy Windows TWRTrainer UI — then paste or upload them. +openfsd already loads TWRTrainer-compatible **`.apt`** (airport geometry) and **`.air`** (scenario aircraft snapshots) into an in-process sweatbox simulator and exposes an instructor control panel at `/sweatbox`. Instructors can also author those files in-product via the integrated map editor. -This design adds a **single, integrated map-first editor** on the openfsd admin Web UI for creating and editing **paired** `.apt` and `.air` files. The editor is a **server-rendered MPA shell** with a **JavaScript-heavy progressive enhancement** (Leaflet map + geometry tools). **No durable server persistence** of `.apt` / `.air` (no disk, no DB): primary load is client-local (FileReader / paste) and primary save is **browser Blob download**. Optional Admin-only echo-download and validate POSTs may hold bodies **transiently in process memory** for the duration of the request only — they never write files or store rows. +This design delivers a **single, integrated map-first editor** on the openfsd admin Web UI for creating and editing **paired** `.apt` and `.air` files. The editor is a **server-rendered MPA shell** with a **JavaScript-heavy progressive enhancement** (Leaflet map + geometry tools). **No durable server persistence** of `.apt` / `.air` (no disk, no DB): primary load is client-local (FileReader / paste) and primary save is **browser Blob download**. Optional Admin-only echo-download and validate POSTs may hold bodies **transiently in process memory** for the duration of the request only — they never write files or store rows. The page targets a dense **16:9 operator console** matching the sweatbox rail aesthetic, reuses in-tree Leaflet assets, and respects the boring-web house standard and the web ↛ sweatbox import-graph edge. @@ -38,20 +56,20 @@ Instructor workflow (target): ## Background & Motivation -### Current state +### Current state (as of implementation) | Surface | Path | Role | |---------|------|------| -| APT parser | `internal/sweatbox/apt.go` `ParseAPT` | TWRTrainer-compatible; non-fatal `[]string` errors | -| AIR parser | `internal/sweatbox/air.go` `ParseAIR` | 16 colon fields; best-effort rows + errors | -| Domain types | `internal/sweatbox/types.go` | `Airport`, `Surface`, `Aircraft`, `Point` | -| Fixtures | `internal/sweatbox/testdata/KBTV_example.{apt,air}` | Golden samples | -| Format (write) | **missing** | No `FormatAPT` / `FormatAIR` today — only parse | -| Sweatbox UI | `internal/web/templates/sweatbox.html` + `sweatbox.css` + `sweatbox.js` | 16:9 rail; forms + PE poll | -| Sweatbox routes | Admin-only under `requireMinRatingHTML(Administrator)` | `/sweatbox`, form POSTs, CSRF | -| Dashboard map | `dashboard.html` + `dashboard.js` + embedded Leaflet 1.9.4 | OSM standard tiles; PE exception documented | -| Import graph | `scripts/check-import-graph.sh` + `Agents.md` | **`internal/web` must not import `internal/sweatbox`** | -| JS tests | **none** | No package.json / vitest / playwright in repo today | +| APT/AIR parse+format | `pkg/twrfiles` (`parse_apt.go`, `parse_air.go`, `format_apt.go`, `format_air.go`) | TWRTrainer-compatible Parse + Format; golden fixtures under `pkg/twrfiles/testdata` | +| Domain types | `pkg/twrfiles` types; sweatbox re-exports/aliases as needed | `Airport`, `Surface`, `Aircraft`, `Point` | +| Sweatbox engine | `internal/sweatbox` | Pure sim; loads via twrfiles | +| Format (write) | **present** | `FormatAPT` / `FormatAIR` with byte goldens | +| Editor UI | `/airport-editor` MPA + Leaflet PE | Map layers, draw/edit, Blob download, validation UX | +| Validate API | `internal/web` + `pkg/twrfiles` | Admin JSON validate-apt/air; no geometry in response | +| Sweatbox UI | `internal/web` sweatbox templates/static | 16:9 rail; forms + PE poll | +| Import graph | `scripts/check-import-graph.sh` + `Agents.md` | **`internal/web` must not import `internal/sweatbox`** (may use `pkg/twrfiles`) | +| JS tests | `webjs/` + `scripts/check-webjs.sh` (CI) | Node ≥20 pure-module tests; sources stay under `internal/web/static/js/openfsd/` | +| Playwright | **Cancelled** | No browser automation suite by house policy | ### Pain points @@ -1272,9 +1290,9 @@ bash scripts/check-webjs.sh ## Open Questions -1. **Should Supervisors access the editor?** Default Admin-only; product call. +1. **Should Supervisors access the editor?** **Resolved for v1:** Admin-only (product default; Supervisor access deferred). 2. **Esri imagery ToS** for each deployer's traffic profile — keep optional and documented. -3. **Undo stack?** Highly useful; implement simple in-memory undo (50 steps) in model if time permits in map PR; otherwise v1.1. +3. **Undo stack?** **Deferred v1.1** — not a v1 ship requirement. 4. **~~Playwright CI~~** — **cancelled.** No browser automation suite. Resolved by this rev: WIP download allowed (yes); fixture path = `pkg/twrfiles/testdata`; dirty = hash + Mark clean; Format contract frozen; API authz = 403 JSON; JS import path relative from `webjs/`. @@ -1402,9 +1420,11 @@ AAL123:B738/F:J:I:KBTV:KBOS:29000:BTV4 MPV LEB MHT:/v/charts:2200:S:44.469758:-7 Each PR independently reviewable; green `go test -race ./...`, gofmt. Import-graph/hygiene: run scripts locally; **recommend adding them to CI in PR1**. Webjs unit tests land as CI in **PR4** (not deferred). +**Closeout:** PR1–8 **Done**; PR9 Playwright **Cancelled**. + --- -### PR 1 — Extract `pkg/twrfiles` (Parse + types + aliases) +### PR 1 — Extract `pkg/twrfiles` (Parse + types + aliases) — **Done** - **Title:** `twrfiles: extract APT/AIR parse types from sweatbox` - **Files/components:** @@ -1420,7 +1440,7 @@ Each PR independently reviewable; green `go test -race ./...`, gofmt. Import-gra --- -### PR 2 — `FormatAPT` / `FormatAIR` + golden contract +### PR 2 — `FormatAPT` / `FormatAIR` + golden contract — **Done** - **Title:** `twrfiles: add FormatAPT/FormatAIR with byte goldens` - **Files:** `pkg/twrfiles/format_apt.go`, `format_air.go`, `testdata/*.formatted.apt|air`, parity cases, tests @@ -1429,7 +1449,7 @@ Each PR independently reviewable; green `go test -race ./...`, gofmt. Import-gra --- -### PR 3 — Airport editor HTML shell + authz + no-JS download +### PR 3 — Airport editor HTML shell + authz + no-JS download — **Done** - **Title:** `web: add /airport-editor shell with echo-download` - **Files:** `routes.go`, `pages_airport_editor.go`, `pagemodel.go`, `templates/airport_editor.html` (fallback fields `apt_text`/`air_text`/`filename`/CSRF), `templates.go`, `layout.html` nav, `airport-editor.css` (layout only), tests, README note @@ -1438,7 +1458,7 @@ Each PR independently reviewable; green `go test -race ./...`, gofmt. Import-gra --- -### PR 4 — JS pure parse/format + Node unit tests + CI +### PR 4 — JS pure parse/format + Node unit tests + CI — **Done** - **Title:** `webjs: APT/AIR parse-format modules with parity tests` - **Files:** @@ -1453,7 +1473,7 @@ Each PR independently reviewable; green `go test -race ./...`, gofmt. Import-gra --- -### PR 5 — Optional server validate API +### PR 5 — Optional server validate API — **Done** - **Title:** `web: Admin validate-apt/air JSON API via pkg/twrfiles` - **Files:** API handlers (inline Admin → **403 JSON**), routes, tests; import-graph allows web → twrfiles @@ -1462,7 +1482,7 @@ Each PR independently reviewable; green `go test -race ./...`, gofmt. Import-gra --- -### PR 6 — Map canvas read-only: load, render, select, fit +### PR 6 — Map canvas read-only: load, render, select, fit — **Done** - **Title:** `web: airport-editor map layers and surface/aircraft lists (read-only)` - **Files:** `main.js`, `map-layers.js`, `ui-rail.js`, `ui-toolbar.js` (open/fit/layer only), CSS; Leaflet + rotated marker; basemap OSM + Esri with **visible attribution**; blank-tile `data-test-tiles` hook for e2e @@ -1471,7 +1491,7 @@ Each PR independently reviewable; green `go test -race ./...`, gofmt. Import-gra --- -### PR 7 — Geometry & aircraft editing + Blob download +### PR 7 — Geometry & aircraft editing + Blob download — **Done** - **Title:** `web: airport-editor draw/edit modes and Blob download` - **Files:** model mutations, draw modes (incl. hold), vertex drag, aircraft place/drag, shortcuts `1`–`6`, `download.js`, dirty hash + Mark clean, `beforeunload`, replace confirms, Raw preview+Apply @@ -1480,7 +1500,7 @@ Each PR independently reviewable; green `go test -race ./...`, gofmt. Import-gra --- -### PR 8 — Validation panel + server confirm + polish +### PR 8 — Validation panel + server confirm + polish — **Done** - **Title:** `web: airport-editor validation UX and docs polish` - **Files:** Validate tab, soft warnings, server validate button, empty states, sweatbox handoff copy, README/wiki residual diff --git a/docs/design/sweatbox-integrated-simulator.md b/docs/design/sweatbox-integrated-simulator.md index 389f9ca..c1c482b 100644 --- a/docs/design/sweatbox-integrated-simulator.md +++ b/docs/design/sweatbox-integrated-simulator.md @@ -5,17 +5,35 @@ | **Document** | Integrated Sweatbox Simulator + Instructor Web UI | | **Author** | _(design author / implementer)_ | | **Date** | 2026-07-17 | -| **Status** | Draft (rev 7 — AfterFunc pointer-safe + stop-before-Cancel) | +| **Status** | **Implemented** (landed P0+P1; see Implementation status) | | **Target repo** | `/Users/rnorris/scratch/openfsd` | | **Reference** | `/Users/rnorris/scratch/openfsd-twrtrainer` (UX + sim semantics only) | --- +## Implementation status + +Shipped. Summary of the tree as of closeout: + +| Area | Location / notes | +|------|------------------| +| Pure engine | `internal/sweatbox` (taxi, pattern, kinematics; apt/air via `pkg/twrfiles`) — coverage floor ≥95% | +| Host / lifecycle | `internal/server/sweatbox_host.go`, `sweatbox_http.go`, `sweatbox_fpl.go` | +| Synthetic sessions | `session.Session` with nil `Conn` + `SenderWorker` drain; registered in postoffice | +| Service HTTP control plane | `/sweatbox/*` under admin service JWT | +| Web instructor UI | `/sweatbox` MPA + PE (`internal/web` pages/templates/static) | +| E2E | `internal/server/e2e_sweatbox_test.go` (+ human mix paths) | +| Product defaults | Admin-only; synthetic CID base 900001; max aircraft cap; datafeed includes synthetics | + +Design history below is retained. Background “current state” tables describe pre-implementation integration surfaces unless marked “as of implementation.” + +--- + ## Overview -openfsd today is a production-shaped FSD server: real TCP pilot/ATC clients, a postoffice registry (`internal/postoffice`), per-connection sessions (`internal/session`), protocol handlers (`internal/server`), and a boring-web admin MPA (`internal/web`) that drives the FSD process via authenticated service HTTP (`internal/server/http_service.go`). +openfsd is a production-shaped FSD server: real TCP pilot/ATC clients, a postoffice registry (`internal/postoffice`), per-connection sessions (`internal/session`), protocol handlers (`internal/server`), and a boring-web admin MPA (`internal/web`) that drives the FSD process via authenticated service HTTP (`internal/server/http_service.go`). -TWRTrainer (legacy VB6) produces multi-aircraft training traffic by opening **one outbound TCP pilot connection per aircraft** into a remote FSD sweatbox. That model is **explicitly rejected** for openfsd. We will implement a **native, in-process sweatbox simulator**: aircraft are synthetic participants registered in the same postoffice as humans; kinematics, taxi, pattern, and instructor commands run inside the FSD process; the admin service HTTP is the sole control plane; a server-rendered instructor UI drives that plane through the existing web → FSD HTTP proxy pattern. +TWRTrainer (legacy VB6) produces multi-aircraft training traffic by opening **one outbound TCP pilot connection per aircraft** into a remote FSD sweatbox. That model is **explicitly rejected** for openfsd. openfsd implements a **native, in-process sweatbox simulator**: aircraft are synthetic participants registered in the same postoffice as humans; kinematics, taxi, pattern, and instructor commands run inside the FSD process; the admin service HTTP is the sole control plane; a server-rendered instructor UI drives that plane through the existing web → FSD HTTP proxy pattern. Result: ATC students connect as normal FSD clients and see sweatbox traffic as ordinary pilots. Instructors never open N pilot sockets. There is no loopback networking tax between simulator and server. @@ -40,22 +58,24 @@ openfsd sweatbox (this design): ## Background & Motivation -### Current openfsd state (integration surfaces) +### Historical pre-implementation integration surfaces + +> Snapshot of openfsd seams **before** sweatbox landed (kept for design motivation). **As of implementation:** pure engine in `internal/sweatbox`; host in `internal/server/sweatbox_*.go`; gnet FSD I/O (no classic accept path); shared login/broadcast helpers in `conn.go`; synthetics use nil-Conn + `SenderWorker`. | Surface | Path | Role for sweatbox | |---------|------|-------------------| -| Session | `internal/session/session.go` | Participant after login; atomic lat/lon/alt/gs/hdg; `Send` / `SendPosition` → `sendChan`; `New` documents `conn == nil` for **unit tests** (production synthetics are a deliberate extension of that nil-Conn path) | +| Session | `internal/session/session.go` | Participant after login; atomic lat/lon/alt/gs/hdg; `Send` / `SendPosition` → `sendChan` or Outbound; `New` documents `conn == nil` for tests and **production synthetics** | | Postoffice | `internal/postoffice/postoffice.go` | Callsign map + geo index; `Register` / `Release` / `UpdatePosition` / `Search` / `All` / `Send` / `Find` / `Snapshot` | | Registry DI | `internal/server/deps.go` | `Registry` interface used by handlers + HTTP | -| Service HTTP | `internal/server/http_service.go` | JWT `TokenType == "fsd_service"`, rating ≥ Administrator; today: `GET /online_users`, `POST /kick_user` | +| Service HTTP | `internal/server/http_service.go` + `sweatbox_http.go` | JWT `TokenType == "fsd_service"`, rating ≥ Administrator; `GET /online_users`, `POST /kick_user`, `/sweatbox/*` | | Broadcast | `internal/server/util.go` | `broadcastRanged`, `broadcastRangedVelocity`, `broadcastRangedAtcOnly`, `broadcastAll`, `broadcastAllATC`, `broadcastAllSupervisors`, `sendDirectOrErr` | -| Conn lifecycle | `internal/server/conn.go` | Real path: login → `Register` → `SenderWorker` → `broadcastAddPacket` → `eventLoop` → defer `Release` + `broadcastDisconnectPacket` | -| Cancel-only paths | `http_service.go` kick, `handler_admin.go` `$!!`, `handler_delete.go` `#DP` | Humans leave via `handleConn` defers; **Cancel alone does not Release** — critical for synthetic design | -| Web proxy | `internal/web/data.go` `makeFsdHttpServiceHttpRequest` | Mints short-lived `fsd_service` JWT and calls FSD service HTTP | -| Dashboard | `internal/web/pages_dashboard.go` + Leaflet PE | Online users already surface via service HTTP | -| E2E | `internal/server/testserver.go`, `e2e_test.go`, `pkg/fsdclient` | Real TCP clients + service JWT helpers (`MakeServiceJWT`) | +| Conn lifecycle | `internal/server/gnet_fsd.go` + `conn.go` helpers | Real path: gnet login → `Register` → CoalesceOutbound → MOTD/broadcastAdd; synthetics: host register + `SenderWorker` + broadcast helpers | +| Cancel-only paths | `http_service.go` kick, `handler_admin.go` `$!!`, `handler_delete.go` `#DP` | Humans leave via gnet disconnect cleanup; **Cancel alone does not Release** — critical for synthetic design | +| Web proxy | `internal/web` FSD service HTTP client | Mints short-lived `fsd_service` JWT and calls FSD service HTTP | +| Dashboard | `internal/web/pages_dashboard.go` + Leaflet PE | Online users (incl. synthetics) via service HTTP | +| E2E | `internal/server/testserver.go`, `e2e_*.go`, `pkg/fsdclient` | Real TCP clients + service JWT helpers | | Protocol | `pkg/protocol` | `@` pilot position, `#AP`/`#DP`, `$FP`, `#TM`, etc. | -| FPL storage | `handler_flightplan.go` + `util.go` | `FlightPlan` atomic holds **info section only** (fields after SOURCE:DEST), not full `$FP` line | +| FPL storage | `handler_flightplan.go` + util | `FlightPlan` atomic holds **info section only** (fields after SOURCE:DEST), not full `$FP` line | ### TWRTrainer (what we learn, not what we copy) @@ -1286,9 +1306,11 @@ Lighter than full Participant refactor; still touches postoffice. **Deferred** Each PR independently reviewable; green `go test -race ./...`, gofmt, hygiene, import-graph. +**Closeout:** all planned PRs below are **Done** (or pattern polish deferred only as product scope already shipped). Status notes added at each PR header. + --- -### PR 1 — `internal/sweatbox`: airport + scenario parsers +### PR 1 — `internal/sweatbox`: airport + scenario parsers — **Done** - **Title:** `sweatbox: add .apt/.air parsers and KBTV fixtures` - **Files:** `internal/sweatbox/apt.go`, `air.go`, `types.go`, `testdata/`, unit tests; `scripts/check-import-graph.sh` (sweatbox forbidden edges + **web → sweatbox**); **`Agents.md` §1 package table + §2 forbidden edges** for `internal/sweatbox` (stdlib + `internal/geo` only; must not import session/postoffice/server/web/db/auth/metar/fsdclient) @@ -1297,7 +1319,7 @@ Each PR independently reviewable; green `go test -race ./...`, gofmt, hygiene, i --- -### PR 2 — `internal/sweatbox`: airport graph + taxi routing +### PR 2 — `internal/sweatbox`: airport graph + taxi routing — **Done** - **Title:** `sweatbox: intersection graph and taxi planner` - **Files:** `airport.go`, `taxi.go`, tests @@ -1306,7 +1328,7 @@ Each PR independently reviewable; green `go test -race ./...`, gofmt, hygiene, i --- -### PR 3a — `internal/sweatbox`: engine skeleton + lifecycle commands +### PR 3a — `internal/sweatbox`: engine skeleton + lifecycle commands — **Done** - **Title:** `sweatbox: engine skeleton with add/del/pause/ops` - **Files:** `aircraft.go`, `engine.go`, `snapshot.go`, minimal `command.go`/`dispatch.go` @@ -1315,7 +1337,7 @@ Each PR independently reviewable; green `go test -race ./...`, gofmt, hygiene, i --- -### PR 3b — `internal/sweatbox`: ground movement commands +### PR 3b — `internal/sweatbox`: ground movement commands — **Done** - **Title:** `sweatbox: taxi/hold/cross/cto command dispatch` - **Files:** dispatch extensions, tests @@ -1324,7 +1346,7 @@ Each PR independently reviewable; green `go test -race ./...`, gofmt, hygiene, i --- -### PR 3c — `internal/sweatbox`: air vectors + flight-plan fields +### PR 3c — `internal/sweatbox`: air vectors + flight-plan fields — **Done** - **Title:** `sweatbox: fh/cm/spd and fp/vp domain updates` - **Files:** dispatch + aircraft plan fields (domain only, not wire encode) @@ -1333,7 +1355,7 @@ Each PR independently reviewable; green `go test -race ./...`, gofmt, hygiene, i --- -### PR 4 — `internal/sweatbox`: motion tick +### PR 4 — `internal/sweatbox`: motion tick — **Done** - **Title:** `sweatbox: kinematics tick for taxi and vectors` - **Files:** `motion.go`, `Tick`, tests @@ -1342,7 +1364,7 @@ Each PR independently reviewable; green `go test -race ./...`, gofmt, hygiene, i --- -### PR 5 — Synthetic flag, broadcast skip, Conn helper +### PR 5 — Synthetic flag, broadcast skip, Conn helper — **Done** - **Title:** `session: Synthetic flag; skip synthetic recipients on all fan-out` - **Files:** `session.go` (`Synthetic`, optional `RemoteIP`); `util.go` **all six** broadcast helpers; tests in `util_extra_test.go` @@ -1356,7 +1378,7 @@ Each PR independently reviewable; green `go test -race ./...`, gofmt, hygiene, i --- -### PR 6 — SweatboxHost + lifecycle + DI + tick apply +### PR 6 — SweatboxHost + lifecycle + DI + tick apply — **Done** - **Title:** `server: SweatboxHost registers synthetic pilots with Cancel-safe lifecycle` - **Files:** `sweatbox_host.go`, `sweatbox_fpl.go`, `server.go`, `deps.go`, `config.go`, `handler_admin.go`, `http_service.go` kick branch, `New`/`NewDefault`/`StartTestServer` @@ -1384,7 +1406,7 @@ Each PR independently reviewable; green `go test -race ./...`, gofmt, hygiene, i --- -### PR 7 — Service HTTP `/sweatbox/*` +### PR 7 — Service HTTP `/sweatbox/*` — **Done** - **Title:** `server: service HTTP sweatbox API (gated)` - **Files:** `sweatbox_http.go`, route registration when enabled, DTOs, tests, 2 MiB body limit @@ -1393,7 +1415,7 @@ Each PR independently reviewable; green `go test -race ./...`, gofmt, hygiene, i --- -### PR 8 — E2E human ↔ sweatbox +### PR 8 — E2E human ↔ sweatbox — **Done** - **Title:** `server: e2e sweatbox visibility, FP, delete, kick/kill lifecycle` - **Files:** `e2e_sweatbox_test.go`, testserver helpers @@ -1402,7 +1424,7 @@ Each PR independently reviewable; green `go test -race ./...`, gofmt, hygiene, i --- -### PR 9 — Web MPA instructor UI (no-JS primary) +### PR 9 — Web MPA instructor UI (no-JS primary) — **Done** - **Title:** `web: boring-web sweatbox instructor page` - **Files:** `pages_sweatbox.go`, `templates/sweatbox.html`, nav, routes, CSRF forms, flash PRG, multipart limits, tests @@ -1411,7 +1433,7 @@ Each PR independently reviewable; green `go test -race ./...`, gofmt, hygiene, i --- -### PR 10 — Web PE live refresh +### PR 10 — Web PE live refresh — **Done** - **Title:** `web: sweatbox live refresh progressive enhancement` - **Files:** `sweatbox.js`, `/api/v1/sweatbox/*` proxies with jwt+CSRF, optional map @@ -1420,7 +1442,7 @@ Each PR independently reviewable; green `go test -race ./...`, gofmt, hygiene, i --- -### PR 11 — Pattern flying (P1) +### PR 11 — Pattern flying (P1) — **Done** - **Title:** `sweatbox: pattern legs and P1 command set` - **Files:** `pattern.go`, dispatch, motion, unit + e2e smoke @@ -1429,7 +1451,7 @@ Each PR independently reviewable; green `go test -race ./...`, gofmt, hygiene, i --- -### PR 12 — Polish: badge, coverage floor, ops UX +### PR 12 — Polish: badge, coverage floor, ops UX — **Done** - **Title:** `sweatbox: synthetic badge, coverage gate, ops polish` - **Files:** `OnlineUserPilot.Synthetic`, dashboard badge optional, **`check-coverage.sh` ≥95% for `internal/sweatbox`**, optional stress tag 30 AC