phase: 04_controller_page_responsive

**Final verification pass** (all three task reports PASS; tasks already in `complete/`). One real defect found and fixed.

**Commands / results.** `bash tests/run_tests.sh` → `RESULT: all suites passed`, **5 104** checks (page **220**). `SANITIZE=1 bash tests/run_tests.sh` → passed. `bash .agents/validate.sh` → `validation OK`; bridge/console/rollback `SUCCESS`; `page bytes: 59262`; `README/AGENTS claims: OK`; `repo hygiene: OK`. Real-browser leg (headless Chromium, served assembled bytes): no horizontal scroll at 360/390/768/1440, ARM first-in-`main` and sticky, log height-capped, all 5 folds openable, all 3 changed page statements execute.

**Fix.** The folded notes' `<summary>` — the only route to five pinned safety sentences — rendered **17 px** on coarse pointers, under the page's own 44 px thumb floor, while every byte-level assertion stayed green. `src/web_page.h`: `button.k,.pill,input,summary{min-height:44px}` + `summary{padding:13px 0}`; `tests/test_page.js` +1 check (219→220); mutation-tested (reverting the CSS fails host *and* browser legs, then restored byte-for-byte).

**Criteria.** 1 ✓ browser-measured (physical phone still unobserved). 2 ✓ 220 checks + prose untouched. 3 ✓ page 59 286→**59 262 B** (−24 vs phase start), README = gate figure. 4 ✓ one `test_page` row at 220, intro 5 104 = measured. 5 ✓ gate green; phases 01–03 assertions unchanged apart from the re-pinned byte constant.

**Docs.** Re-pinned 59 262 (README, `config.h`, `test_conn.cpp`), totals 5 103→5 104, README Tests row 220; re-measured footprint rows (flash 1 257 901 / 1 178 141 / 1 157 593, TLS +6 176/+100 308); fixed stale "48 KB"/"~50 KB" (×4 src comments, ×2 README) and the 51 626-byte page-delivery row; added **bug 14** with root cause and renamed *Thirteen*→*Fourteen bugs* in README + AGENTS.md's two stale references; added the Phase 04 browser+device row to `.agents/HW_VERIFICATION.md`.

**Deviations.** Device leg stays open: live board serves 51 626 B (pre-phase image); no reflash attempted, per the standing no-unattended-reflash rule — no byte-equality claim made. `ruff check tools tests`: 29 findings, identical at HEAD and `--isolated`, `E9,F` clean, no repo lint config or gate step → left untouched. `.agents/validate.sh` is **untracked and has never been committed** (`git log --all --` empty) though README/AGENTS/PLAN cite it 13× — harness needs `git add` it; I did not stage anything.
This commit is contained in:
2026-11-09 11:00:27 -05:00
parent d629a0ff51
commit c43a088ce8
30 changed files with 1600 additions and 214 deletions
@@ -0,0 +1,70 @@
# Phase 04 — The controller page: less to read, works on a phone
**Source:** `TODO.md` L4 — "Clean up keyrelay interface, don't need as much text, make it
responsive"
**Story:** `n/a`
**Context:** `src/web_page.h` (markup + `<style>` + script), `tests/test_page.js`, README
"Controller page"; `AGENTS.md` "The page is served from flash"
## Objective
Reorganise the page so the controls are the thing you see: single column and thumb-sized on a
phone, unchanged on a laptop, and the explanatory prose folded away to where it is asked for
instead of sitting between the operator and the ARM button. **No safety statement gets
weaker or shorter** — they move, they do not shrink, and each one gets a test that fails if it
disappears.
## Why the prose is load-bearing anyway
Every long hint in this page exists because someone got hurt without it: the arming banner
(Enter goes to another machine), the `Esc`-frees-the-pointer note (Pointer Lock is invisible),
the layout caveat (characters are positions), the KDE paragraph (those three buttons are the
laptop's until the board steals them). Trimming them is the right call for usability and the
wrong call for safety *unless* the trimming is pinned. So this phase writes the assertions
before it deletes the sentences.
## Dependencies
- `03_session_takeover` (todo) — its TAKE OVER affordance and the `tls` pill/fingerprint line
are new elements in the same markup; folding prose around them is easier once they exist,
and the id-coverage check has to be updated once.
## Tasks
1. `01_responsive_layout.md` — CSS and viewport only: breakpoints, thumb-sized targets, sticky
ARM bar, safe-area insets, collapsed log; no markup restructure, no id changes.
2. `02_prose_trim_pinned.md` — the safety assertions first, then the fold: long hints into
`<details>`, the footer shortened, the connection/mouse/paste paragraphs cut to one line
each with the reasoning one tap away.
3. `03_suite_size_and_docs.md` — `test_page.js` extended for the new structure, the README's
page-size claim and its duplicated `test_page` row corrected, totals made to agree, and the
`curl` byte check re-derived.
## Testing & Quality
- `tests/test_page.js` is the only thing that can see this page before a browser does
(`AGENTS.md`: a raw string is invisible to every compiler). It must cover, in addition to
what it covers today:
- **safety strings still present**: the arming warning names Enter and "every key"; the
mouse hint names `Esc` and release; the paste hint names "stop typing"/disarm cancelling a
run; the layout note says positions/US layout; the default-passphrase shout still exists.
- every element id the script reaches for exists in the markup (existing rule, re-verified
after the restructure);
- the pinned page constants (`TEXT_FRAME_MAX`, `TEXT_JOB_MAX`) still equal `config.h`;
- the script still behaves identically: arming, state frames, paste chunking, mouse
capture/movement/button ordering, `E12`.
- Coverage **>90%** on the page script's changed branches; CSS is checked by assertions on the
served bytes (breakpoints present, sticky ARM bar) plus a phone observation.
- Device (best effort, one attempt): `curl -sk https://keyrelay.local/ | wc -c` equals the
byte count the gate prints, and a phone shows one column with the ARM bar reachable and the
log not pushing it off screen.
## Completion Criteria
- [ ] The page works at 360 px wide and at 1440 px wide without horizontal scroll, ARM is
reachable with a thumb, and the log is collapsible.
- [ ] Every safety statement above is still on the page **and** asserted by `test_page.js`.
- [ ] The page is smaller in bytes than it is today, and README's size claim matches.
- [ ] README Tests table has one `test_page` row with the real count (the duplicate stale row
at 47 checks is removed), and the intro's total check count matches the suite output.
- [ ] `bash .agents/validate.sh` green; no behaviour change in phases 01–03.
@@ -0,0 +1,66 @@
# Task 01 — Responsive layout: CSS and viewport, nothing else
**Phase:** `04_controller_page_responsive` · **Source:** `TODO.md:4` — "Clean up keyrelay
interface, don't need as much text, make it responsive" (the layout half)
**Story:** `n/a`
## Objective
The page lays out for the screen it is on: one column and thumb-sized controls on a phone, the
current dense grid on a laptop, the ARM bar always reachable, the viewport configured so the
layout is not scaled-bitmap small.
## Work
1. `src/web_page.h`, `<style>`:
- `main` grid: keep `auto-fit`/`minmax(240px,1fr)` above the breakpoint; at `@media (max-width:
720px)` force one column and reduce padding.
- Thumb targets: `button.k`, `input`, `.pill`, checkboxes get `min-height:44px` (and
`min-width` where a target is narrower) on coarse pointers —
`@media (pointer: coarse)` where that reads better than a width query.
- `input[type=password], input[type=text], textarea { font-size:16px }` on small screens:
below 16 px iOS Safari zooms the page on focus and never zooms back out. Name that in the
comment — it is why the size is not "13 px looks nicer".
- ARM bar: `position:sticky; top:0; z-index` so it survives scrolling on a phone; keep the
armed styling exactly as it is (`body.armed` outline is an indicator, not decoration).
- `#log`: `max-height` on small screens, and `overscroll-behavior:contain` so scrolling the
log does not scroll the page.
- `touch-action:manipulation` on the interactive elements (kills the 300 ms double-tap
zoom) and `user-select:none` where a press is a control, not text.
- `padding-bottom: calc(14px + env(safe-area-inset-bottom))` on `main`, plus
`viewport-fit=cover` added to the existing `<meta name="viewport">` (without it `env()`
is always 0). Do **not** add `maximum-scale`/`user-scalable=no` — pinch zoom stays.
2. `tests/test_page.js` — assertions on the served bytes, in the existing style: the viewport
meta contains `viewport-fit=cover` and does not disable scaling; a `max-width:720px`
breakpoint exists; the ARM bar is sticky; the coarse-pointer target floor exists; the
input font-size floor exists. These are layout claims, so they are checked where the page
can be seen without a browser.
3. `src/web_page.h` header comment — one paragraph: the page is for a laptop *and* a phone
held in one hand over a target you are standing at, and the sticky ARM bar exists because
the state must be unmissable at any scroll position.
- ASSUMPTION: breakpoints at 720 px plus a `pointer: coarse` query, rather than a full design
system or container queries. One breakpoint the README can describe beats five nobody can
remember.
- ASSUMPTION: dark theme only, as today. A light theme is not on anyone's list and doubles
every colour assertion.
- ASSUMPTION: the log stays in the page (collapsed by its own height, not hidden behind a tab)
because the log is where `E9`/`E4` explanations appear; hiding it is hiding the diagnostics.
## Testing & Quality
- `node tests/test_page.js` green, including the new byte-level layout assertions; the id
coverage check unchanged (no ids added or renamed by this task).
- `bash .agents/validate.sh` green; the page byte count printed by the gate changes and is
recorded (the README claim is task 03's job).
- Device (best effort, one attempt): a phone at 360–430 px logical width shows one column, the
ARM bar stays visible while scrolling, focusing the passphrase field does not zoom the page.
## Completion Criteria
- [ ] No horizontal scroll at 360 px, 768 px or 1440 px (observed on a phone or recorded as
`skipped (no device)` with the exact check).
- [ ] ARM reachable without scrolling from the top of the page on a phone, and it is still the
first element in `main`.
- [ ] Focus on the passphrase input does not zoom (or the report says unobserved).
- [ ] All four layout assertions present in `test_page.js`; full test suite green.
@@ -0,0 +1,70 @@
# Task 02 — Cut the reading, keep the warnings: assertions first
**Phase:** `04_controller_page_responsive` · **Source:** `TODO.md:4` — "Clean up keyrelay
interface, **don't need as much text**, make it responsive" (the prose half)
**Story:** `n/a`
## Objective
The page stops being a wall of explanation while every safety statement it carries survives
verbatim — each one now guarded by a test that fails if a later edit deletes it. Write the
assertions, then delete the prose.
## Work
1. `tests/test_page.js` — **first**, before touching the markup: add a named check per
statement that must survive, matching on normalised text so a rewrap does not trip it:
- arming forwards **every** key including Enter, and only while armed;
- `Esc` frees the pointer and releasing frees the buttons;
- disarm / stop / focus loss cancels a run of pasted text;
- characters are positions → the target must be on a US layout;
- the default-passphrase shout (driven by `dflt:1`, not static text);
- nothing is stored/scheduled/replayed, in whatever wording survives;
- the takeover cannot arm (added by phase 03 — guard it now so this task cannot lose it).
Run the suite: it must pass *before* the trim (proving the sentences are there) and after
(proving the trim did not remove them).
2. `src/web_page.h` markup:
- Each long paragraph becomes a one-line summary plus `<details><summary>why</summary>…`:
the KDE/snap-left explanation, the mouse "movement is relative" paragraph, the send-text
paragraph, the chords paragraph, and the footer's shortcut list. The **arming banner**
stays visible — it is the one paragraph that must never be behind a tap.
- The Connection card's passphrase paragraph → one line ("the board checks this on every
connection; without it nothing can arm") with the rest in `<details>`.
- Shorten `title` attributes that are currently multi-sentence (they are already duplicated
in the `<details>`); a `title` is a tooltip, not a manual.
- Collapse the Log card's option row and the State card's `kv` rows into a two-line summary
at narrow widths — counters stay visible; they are the "is it typing" evidence.
- Keep the DOM ids and the class hooks the CSS in task 01 targets; keep
`#banner`, `#arm`, `#log`, `#paste`, `#mPad` and the chord buttons' `data-chord`
attributes byte-identical (`test_proto.cpp` asserts every labelled chord against the
combination its label promises).
3. `<details>` in the shim — `tests/test_page.js`'s DOM shim is id-based, so `<details>` needs
nothing to parse, but the safety assertions must read text content across it: make the
matcher pull from the raw markup string (which the suite already has for the id check)
rather than from the shim's element tree.
4. `README.md` — Controller page section: say the hints are folded and that the safety notes
are pinned by `tests/test_page.js` ("the test asserts the warning is still on the page"),
which is the kind of sentence that keeps the next person from tidying them away.
- ASSUMPTION: `<details>` rather than a help modal or a tour: zero script, keyboard
accessible, printable, works with no JS, and it degrades to visible text in a browser that
ignores it — which is the safe direction for a safety note.
- ASSUMPTION: "don't need as much text" means the *explanations*, not the controls. The chord
buttons, the VT keys and the KDE row stay: the operator who does not need them today needs
them the day a window manager steals Alt+Tab.
## Testing & Quality
- The new safety-string assertions pass before and after the trim; both runs in the report.
- Every one of them is demonstrated to fail when the corresponding sentence is deleted (then
restored) — otherwise they are decoration, which is the failure mode `AGENTS.md` calls out.
- `node tests/test_page.js`, `tests/test_proto.cpp` (chord labels!) and the sanitiser run
green; page byte count drops and is recorded for task 03.
## Completion Criteria
- [ ] Safety statements present and pinned; each pin proven to fail on deletion.
- [ ] Visible prose on the laptop view is roughly a third of what it was; every removed
paragraph is reachable in a `<details>` in the served bytes.
- [ ] `data-chord` bytes, `#banner`, `#arm`, `#paste`, `#mPad` and every script-reached id intact.
- [ ] Full test suite green; `test_proto.cpp` untouched.
@@ -0,0 +1,61 @@
# Task 03 — Make the numbers agree again: suite coverage, page size, README
**Phase:** `04_controller_page_responsive` · **Source:** `TODO.md:4` — "Clean up keyrelay
interface, don't need as much text, make it responsive" (the verification half: a page claim
is only true when something measures it)
**Story:** `n/a`
## Objective
The restructure leaves the documentation and the suite telling the truth about the page as it
is now: one `test_page` row with the real count, the size claim matching the served bytes, and
coverage for the branches the restructure touched.
## Work
1. `tests/test_page.js` — coverage for the restructured page:
- id coverage after the restructure (the rule already exists; make sure it still runs
against the **markup** and not the shim, which hands out an element for any id);
- the `<details>` content is still in the served bytes (a folded note is not a deleted one);
- pinned constants: `TEXT_FRAME_MAX`/`TEXT_JOB_MAX` in the page still equal `kTextFrameMax`
/ `kTextJobCap` in `src/config.h` (the suite does this today — re-run it and confirm it
did not quietly stop matching after the edit);
- behaviour unchanged: arming, `S=` state frames, paste chunking and refusal, mouse capture
and button ordering, `E12`, the fingerprint line, `busy`.
2. `README.md`:
- Tests table: **one** `test_page` row, its real count, description covering the mouse, the
takeover and the folded safety notes. Delete the stale duplicate row that still claims 47
checks — the table currently lists the same suite twice with two different numbers, which
is exactly the "two paths that can disagree" shape `AGENTS.md` warns about.
- Controller page: "~35 KB" becomes the measured number, with the sentence that says how it
was measured (`curl -sk https://keyrelay.local/ | wc -c` against the byte count
`.agents/validate.sh` prints).
- Intro bullet `**3406 checks**` → the new total from the suite output, not arithmetic.
- Quick start / Troubleshooting: any sentence describing the old layout ("the page shows a
banner under the ARM bar", pill lists) corrected to what the page does now.
3. `AGENTS.md` — stale statements only: the page-size check line already changes in phase 02
(`https -k`); if the byte count is quoted anywhere, update it or point at the gate that
prints it rather than restating a number that will drift again.
4. `.agents/HW_VERIFICATION.md` — fill the phone row: 360 px, one column, ARM reachable, no
focus zoom, folded notes expandable — or `skipped (no device)` with the commands.
- ASSUMPTION: the totals in README are taken from a captured suite run pasted into the report,
not added by hand. The intro count and the table sum have disagreed before.
## Testing & Quality
- `bash tests/run_tests.sh` and `SANITIZE=1 tests/run_tests.sh` green; `bash
.agents/validate.sh` green including both firmware builds.
- The README's page-size claim and check count are reproducible from commands in this
repository, and the report shows both commands with their output.
- No change to `src/*.h` other than `src/web_page.h` markup/style; no behaviour change in
phases 01–03 (spot-check TLS, `busy`, and the paste path).
## Completion Criteria
- [ ] One `test_page` row in the Tests table with the real count; no duplicate anywhere.
- [ ] README page size == the served page's byte count (or the report says the board was
unreachable and quotes the PROGMEM count the gate prints).
- [ ] Intro check total matches the suite's own output.
- [ ] `test_page.js` covers: id coverage, folded content present, pinned constants, behaviour
unchanged; full test suite green.