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:
@@ -94,8 +94,9 @@ identity. The instrumentation stays worth having — it is what makes the next c
|
||||
|
||||
| claim | command | expected observation | outcome |
|
||||
|---|---|---|---|
|
||||
| page still byte-exact after the restructure | the generic `curl … \| wc -c` row above | equals the gate's new `page bytes:` count (the count moves when the page does — that is fine; equality with the gate is the check) | |
|
||||
| one column on a phone, ARM thumb-reachable | open `https://keyrelay.local/` on a phone at ~360 px | single column, no horizontal scroll, sticky ARM bar on screen, log collapsed and not pushing ARM off screen | |
|
||||
| page still byte-exact after the restructure | the generic `curl … \| wc -c` row above | equals the gate's new `page bytes:` count (the count moves when the page does — that is fine; equality with the gate is the check) | **skipped (board not reflashed), 2026-11-09 (task 01)** — one capped attempt was run *before* the page edit: `timeout 15 curl -sk https://keyrelay.local/ \| wc -c` → **51 626** and `sha256 dd16e195…f04e9`, byte-identical to the phase-02 close record, so the live board still runs pre-phase-03 firmware and the measurement path works. The 60 780-byte page of this tree is only served after `run -e bridge -t upload`, deliberately not run here: the board is a live HID keyboard into the machine running this pipeline and `to_bootloader.py` strands it if anything goes wrong unattended (same call phase 02 recorded). To close: reflash, then `timeout 15 curl -sk https://keyrelay.local/ \| wc -c` must equal the gate's `page bytes:` line for this tree. **Task 02** (the prose fold) re-ran that one capped read-only probe after the edit: **51 626** bytes, `sha256 dd16e1954a68…` — identical to the task-01 and phase-02 records, so the live board still runs the pre-phase-03 image and this tree's **59 223**-byte page (the gate's `page bytes:` line, equal in both builds) is not on it yet. Measurement path proven, byte comparison still open until a flash. **Task 03** (docs and suite) re-ran the same single capped probe: `timeout 15 curl -sk https://keyrelay.local/ | wc -c` → **51 626**, `sha256 dd16e1954a68…f04e9` — byte-identical to the task-02 record, and this tree's locally assembled page hashes `dc26b441…b634` at **59 223** B (the count the gate prints and the count README's Controller-page section now quotes with this exact `curl` command named as the device-side measurement). README == gate == assembled bytes; device equality still opens by the standing no-unattended-reflash call (live HID keyboard into the pipeline host) | **Final pass, 2026-11-09:** one more capped read-only probe — `timeout 15 curl -sk https://keyrelay.local/ \| wc -c` → **51 626**, `sha256 dd16e195…f04e9`, byte-identical to the task-01/02/03 records (the live board has not been reflashed all phase), while this tree assembles to **59 262 B** / `b9414d2f9847b774…` after the `summary` thumb-floor fix; README (×2), `config.h` (×2) and `test_conn.cpp`'s `kServedPageBytes` are re-pinned to 59 262 and the gate's `page bytes:` line agrees. |
|
||||
| one column on a phone, ARM thumb-reachable | open `https://keyrelay.local/` on a phone at ~360 px | single column, no horizontal scroll, sticky ARM bar on screen, log collapsed and not pushing ARM off screen | **skipped (no device), 2026-11-09 (task 01); still skipped for task 02, 2026-11-09** — no phone is reachable headlessly, and the page must be reflashed first (row above). Task 02 adds one thing for whoever runs it: the folded notes (`<details>` under Special keys, Connection, Mouse, Send text and the footer) must be readable and tappable at 360 px, and the State card's counters must wrap into a couple of lines rather than stacking eight rows — the counters stay on screen because they are the "is it typing" evidence. Host-side the layout is pinned instead: 17 new `test_page.js` checks assert the served bytes carry `viewport-fit=cover` with pinch zoom intact, the one 720 px breakpoint (one column; `main` as flex column, which is what makes the sticky ARM bar actually stick), the 16 px input floor (focus-zoom), the log height cap + `overscroll-behavior:contain`, the 44 px `pointer:coarse` floors, safe-area bottom padding, and ARM still first in `main`. To close: reflash `bridge`, open `https://keyrelay.local/` on a ~360 px phone — one column, no horizontal scroll at 360/768/1440 px, ARM bar visible while scrolling, focusing the passphrase field does not zoom the page. **Task 03** (2026-11-09): still `skipped (no device)` — no phone is reachable headlessly and the page must be reflashed first. The phone checklist for whoever runs it, from the phase's completion criteria: 360 px width, one column, no horizontal scroll, ARM bar reachable with a thumb at the top of the scroll, focusing the passphrase input does not zoom, and each folded note (Special keys, Connection, Mouse, Send text, footer shortcuts) expands with a tappable summary. Host-side task 03 adds the structural teeth for the last one: `test_page.js` pins exactly five `<details>`, each with a summary naming what is behind the tap and real sentences inside, and proves the fold checks fail on deletion (mutation-run, then restored byte-for-byte) |
|
||||
| the responsive layout, observed in a real browser at phone widths (final pass) | assemble the served bytes from `src/web_page.h` (raw-string chunks + each marker branch in turn), serve them on `127.0.0.1`, and measure them in headless Chromium at 360×640, 390×844, 768×1024 and 1440×900 with touch/`pointer:coarse` emulated | no horizontal scroll at any width; ARM reachable without scrolling and sticky at the bottom of a 400-line log; one column and a height-capped, self-scrolling log below the breakpoint; every press target ≥ 44 px on a coarse pointer; the five folds open on a tap and stay inside the viewport; no script error; the safety sentences present in *rendered text* — and the same measurement against the pre-phase-04 page, where the sticky/flex-column/log-cap/thumb-floor rows must fail | **ok (browser, not a phone), 2026-11-09 (phase-04 final pass)** — all five viewports pass on this tree's page (59 262 B, both marker branches equal); the same script on the phase-start page (59 286 B) fails exactly the four rows the phase changed: ARM not sticky (`z-index:auto`, scrolled to −1989 px), `main` still `display:grid`, no log cap, and 19 targets at 34 px. **It also found a defect the byte-level suite could not see, now fixed:** the five `<summary>` elements task 02 introduced rendered **17 px** tall on a coarse pointer — the only route to five pinned safety sentences, on a control below the page's own 44 px thumb floor, with every served-byte assertion green. `summary` is now in the `pointer:coarse` rule (page 59 223 → 59 262 B, re-pinned in `config.h`/`test_conn.cpp`/README); the floor is asserted in `test_page.js` (219 → 220) **and** measured here, and deleting the rule fails both (demonstrated, then restored byte-for-byte). This is not the phone row below: no hand-held device was involved, `env(safe-area-inset-*)` is 0 on a desktop screen (so the inset is checked in the declared CSS, where it means something), and iOS Safari's focus-zoom stays unobserved. |
|
||||
|
||||
### Phase 05 — soft keyboard and touch pad
|
||||
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
**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.
|
||||
+1
@@ -0,0 +1 @@
|
||||
ok
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
== host suites: bash tests/run_tests.sh ==
|
||||
compiler: g++ inside localhost/ai-sandbox:ubuntu-9a08b1d500b0 (repo mounted at /work)
|
||||
keystate 40 checks, 0 failures
|
||||
proto 2379 checks, 0 failures
|
||||
text 459 checks, 0 failures
|
||||
mouse 48 checks, 0 failures
|
||||
output 153 checks, 0 failures
|
||||
ws_codec 135 checks, 0 failures
|
||||
test_tls_pem 862 checks, 0 failures
|
||||
test_tls_session 118 checks, 0 failures
|
||||
test_conn 280 checks, 0 failures
|
||||
test_claim 102 checks, 0 failures
|
||||
handshake
|
||||
frame encoding
|
||||
frame decoding
|
||||
key table
|
||||
pasted text
|
||||
mouse
|
||||
protocol strings
|
||||
the fail-safe wiring
|
||||
the bridge handover (phase 03)
|
||||
one routing path, two transports
|
||||
the client side of pinning
|
||||
the busy board and --take-over
|
||||
the target-side verifier
|
||||
t= 0.00s >>> Ctrl+Alt+Del <<<
|
||||
relayctl 308 checks, 0 failures
|
||||
the log stays readable
|
||||
characters
|
||||
key names in the log
|
||||
sending text
|
||||
refusing what it cannot type
|
||||
stopping
|
||||
the mouse
|
||||
the connect path
|
||||
the channel pill and the certificate line
|
||||
the busy board and the take-over
|
||||
arming and the S= state frames
|
||||
responsive layout
|
||||
the safety statements that must never be tidied away
|
||||
the folded notes are folded, not deleted
|
||||
the page's constants are the board's constants
|
||||
element ids
|
||||
page 220 checks, 0 failures
|
||||
RESULT: all suites passed
|
||||
|
||||
== host suites: SANITIZE=1 bash tests/run_tests.sh ==
|
||||
compiler: g++ inside localhost/ai-sandbox:ubuntu-9a08b1d500b0 (repo mounted at /work)
|
||||
sanitizers: on
|
||||
keystate 40 checks, 0 failures
|
||||
proto 2379 checks, 0 failures
|
||||
text 459 checks, 0 failures
|
||||
mouse 48 checks, 0 failures
|
||||
output 153 checks, 0 failures
|
||||
ws_codec 135 checks, 0 failures
|
||||
test_tls_pem 862 checks, 0 failures
|
||||
test_tls_session 118 checks, 0 failures
|
||||
test_conn 280 checks, 0 failures
|
||||
test_claim 102 checks, 0 failures
|
||||
handshake
|
||||
frame encoding
|
||||
frame decoding
|
||||
key table
|
||||
pasted text
|
||||
mouse
|
||||
protocol strings
|
||||
the fail-safe wiring
|
||||
the bridge handover (phase 03)
|
||||
one routing path, two transports
|
||||
the client side of pinning
|
||||
the busy board and --take-over
|
||||
the target-side verifier
|
||||
t= 0.00s >>> Ctrl+Alt+Del <<<
|
||||
relayctl 308 checks, 0 failures
|
||||
the log stays readable
|
||||
characters
|
||||
key names in the log
|
||||
sending text
|
||||
refusing what it cannot type
|
||||
stopping
|
||||
the mouse
|
||||
the connect path
|
||||
the channel pill and the certificate line
|
||||
the busy board and the take-over
|
||||
arming and the S= state frames
|
||||
responsive layout
|
||||
the safety statements that must never be tidied away
|
||||
the folded notes are folded, not deleted
|
||||
the page's constants are the board's constants
|
||||
element ids
|
||||
page 220 checks, 0 failures
|
||||
RESULT: all suites passed
|
||||
|
||||
== firmware: platformio run -e bridge -e console -e rollback ==
|
||||
Processing bridge (platform: espressif32@8.0.0; board: esp32-s3-devkitc-1; framework: arduino)
|
||||
--------------------------------------------------------------------------------
|
||||
Verbose mode can be enabled via `-v, --verbose` option
|
||||
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
|
||||
PLATFORM: Espressif 32 (8.0.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
|
||||
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
|
||||
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
|
||||
PACKAGES:
|
||||
- framework-arduinoespressif32 @ 4.30312.261024 (3.3.12)
|
||||
- framework-arduinoespressif32-esp32s3-libs @ 3.3.12
|
||||
- tool-esptool @ 2.50400.0 (5.4.0)
|
||||
- tool-riscv32-esp-elf-gdb @ 12.1.0+20221002
|
||||
- tool-xtensa-esp-elf-gdb @ 12.1.0+20221002
|
||||
- toolchain-riscv32-esp @ 14.2.0+20260121
|
||||
- toolchain-xtensa-esp-elf @ 14.2.0+20260121
|
||||
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
|
||||
LDF Modes: Finder ~ chain, Compatibility ~ soft
|
||||
Found 45 compatible libraries
|
||||
Scanning dependencies...
|
||||
Dependency Graph
|
||||
|-- Adafruit NeoPixel @ 1.15.5
|
||||
|-- ESPmDNS @ 3.3.12
|
||||
|-- USB @ 3.3.12
|
||||
|-- Preferences @ 3.3.12
|
||||
|-- WiFi @ 3.3.12
|
||||
Building in release mode
|
||||
Generating partitions .pio/build/bridge/partitions.bin
|
||||
Retrieving maximum program size .pio/build/bridge/firmware.elf
|
||||
Checking size .pio/build/bridge/firmware.elf
|
||||
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
|
||||
RAM: [=== ] 30.4% (used 99744 bytes from 327680 bytes)
|
||||
Flash: [==== ] 40.0% (used 1257901 bytes from 3145728 bytes)
|
||||
========================= [SUCCESS] Took 0.79 seconds =========================
|
||||
|
||||
Processing console (platform: espressif32@8.0.0; board: esp32-s3-devkitc-1; framework: arduino)
|
||||
--------------------------------------------------------------------------------
|
||||
Verbose mode can be enabled via `-v, --verbose` option
|
||||
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
|
||||
PLATFORM: Espressif 32 (8.0.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
|
||||
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
|
||||
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
|
||||
PACKAGES:
|
||||
- framework-arduinoespressif32 @ 4.30312.261024 (3.3.12)
|
||||
- framework-arduinoespressif32-esp32s3-libs @ 3.3.12
|
||||
- tool-esptool @ 2.50400.0 (5.4.0)
|
||||
- tool-riscv32-esp-elf-gdb @ 12.1.0+20221002
|
||||
- tool-xtensa-esp-elf-gdb @ 12.1.0+20221002
|
||||
- toolchain-riscv32-esp @ 14.2.0+20260121
|
||||
- toolchain-xtensa-esp-elf @ 14.2.0+20260121
|
||||
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
|
||||
LDF Modes: Finder ~ chain, Compatibility ~ soft
|
||||
Found 45 compatible libraries
|
||||
Scanning dependencies...
|
||||
Dependency Graph
|
||||
|-- Adafruit NeoPixel @ 1.15.5
|
||||
|-- ESPmDNS @ 3.3.12
|
||||
|-- USB @ 3.3.12
|
||||
|-- Preferences @ 3.3.12
|
||||
|-- WiFi @ 3.3.12
|
||||
Building in release mode
|
||||
Generating partitions .pio/build/console/partitions.bin
|
||||
Retrieving maximum program size .pio/build/console/firmware.elf
|
||||
Checking size .pio/build/console/firmware.elf
|
||||
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
|
||||
RAM: [== ] 20.6% (used 67352 bytes from 327680 bytes)
|
||||
Flash: [==== ] 37.5% (used 1178141 bytes from 3145728 bytes)
|
||||
========================= [SUCCESS] Took 0.78 seconds =========================
|
||||
|
||||
Processing rollback (platform: espressif32@8.0.0; board: esp32-s3-devkitc-1; framework: arduino)
|
||||
--------------------------------------------------------------------------------
|
||||
Verbose mode can be enabled via `-v, --verbose` option
|
||||
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
|
||||
PLATFORM: Espressif 32 (8.0.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
|
||||
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
|
||||
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
|
||||
PACKAGES:
|
||||
- framework-arduinoespressif32 @ 4.30312.261024 (3.3.12)
|
||||
- framework-arduinoespressif32-esp32s3-libs @ 3.3.12
|
||||
- tool-esptool @ 2.50400.0 (5.4.0)
|
||||
- tool-riscv32-esp-elf-gdb @ 12.1.0+20221002
|
||||
- tool-xtensa-esp-elf-gdb @ 12.1.0+20221002
|
||||
- toolchain-riscv32-esp @ 14.2.0+20260121
|
||||
- toolchain-xtensa-esp-elf @ 14.2.0+20260121
|
||||
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
|
||||
LDF Modes: Finder ~ chain, Compatibility ~ soft
|
||||
Found 45 compatible libraries
|
||||
Scanning dependencies...
|
||||
Dependency Graph
|
||||
|-- Adafruit NeoPixel @ 1.15.5
|
||||
|-- ESPmDNS @ 3.3.12
|
||||
|-- USB @ 3.3.12
|
||||
|-- Preferences @ 3.3.12
|
||||
|-- WiFi @ 3.3.12
|
||||
Building in release mode
|
||||
Generating partitions .pio/build/rollback/partitions.bin
|
||||
Retrieving maximum program size .pio/build/rollback/firmware.elf
|
||||
Checking size .pio/build/rollback/firmware.elf
|
||||
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
|
||||
RAM: [=== ] 28.6% (used 93568 bytes from 327680 bytes)
|
||||
Flash: [==== ] 36.8% (used 1157593 bytes from 3145728 bytes)
|
||||
========================= [SUCCESS] Took 0.79 seconds =========================
|
||||
|
||||
Environment Status Duration
|
||||
------------- -------- ------------
|
||||
bridge SUCCESS 00:00:00.791
|
||||
console SUCCESS 00:00:00.781
|
||||
rollback SUCCESS 00:00:00.790
|
||||
========================= 3 succeeded in 00:00:02.361 =========================
|
||||
|
||||
== page size: src/web_page.h raw strings ==
|
||||
page bytes: 59262 (a reflashed device must serve exactly this many; kIndexHtmlLen is sizeof(kIndexHtml)-1 of these same bytes, both builds)
|
||||
|
||||
== README/AGENTS claims vs what this run measured ==
|
||||
README/AGENTS claims: OK (5 104 checks quoted and measured in both runs; 12 Tests-table rows match their suites; 59 262 page bytes quoted; Backlog contiguous; every ws:// sentence names the rollback)
|
||||
|
||||
== repo hygiene ==
|
||||
repo hygiene: OK (131 tracked files, no ignored-tracked files, no key-material shapes, no PII hits)
|
||||
|
||||
== hardware context (informational; cannot change the gate result) ==
|
||||
hw-probe serial /dev/ttyACM0 present, readable (NOT opened: opening toggles DTR/RTS and resets the board)
|
||||
hw-probe mdns keyrelay.local resolves -> 10.2.0.22
|
||||
hw-probe https https://keyrelay.local/ -> HTTP 200 (TLS listener is up)
|
||||
hw-probe http http://keyrelay.local/ -> HTTP 301, redirects to https://keyrelay.local/ (redirect-only: the post-phase-02 shape)
|
||||
hw-probe tls 10.2.0.22:443 negotiated TLSv1.2 / ECDHE-ECDSA-AES256-GCM-SHA384 (a self-signed verify failure is the expected shape; the fingerprint to compare it against is on the serial console)
|
||||
hw-probe uv /home/ducoterra/.local/bin/uv — firmware builds available
|
||||
hw-probe node /usr/bin/node — test_page.js runs
|
||||
hw-probe evdev board 046d:c33f visible: keyboard=/dev/input/event19 (readable) mouse=/dev/input/event20 (readable) — python3 tools/watch_target.py runs ON THIS machine (the target)
|
||||
validation OK (suites + sanitised suites + bridge/console/rollback builds + page size
|
||||
+ the numbers README/AGENTS quote + hygiene)
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
**Task 01 — responsive layout: done.**
|
||||
|
||||
**Implemented**
|
||||
- `src/web_page.h` `<style>`: viewport `viewport-fit=cover` (no `maximum-scale`/`user-scalable`); `main` gains `calc(14px + env(safe-area-inset-bottom))`; `.arm` sticky `top:0;z-index:5` + `touch-action:manipulation`/`user-select:none` (also on `button.k`, `label.opt`, `.mousepad`); `#log` `overscroll-behavior:contain`; `@media (max-width:720px)` → `main` flex column (what makes the sticky ARM bar actually stick — sticky grid items never move), `.grid` forced `1fr`, tighter padding, `font-size:16px` inputs (iOS focus-zoom, named in the comment), log `max-height:40vh`; `@media (pointer:coarse)` → 44 px min-height floor on `button.k`/`.pill`/`input`, min-width floor on chord buttons/checkboxes. No markup/id changes.
|
||||
- `tests/test_page.js`: +17 byte-level layout checks (viewport-fit, no zoom lock, 720 px breakpoint + one column + flex-column switch, 16 px floor, log cap, coarse floors, sticky ARM, overscroll, safe-area calc, touch-action, user-select, ARM first in `main`).
|
||||
- Header comment paragraph on laptop+one-handed-phone layout and why the ARM bar is sticky.
|
||||
- Re-pinned page size everywhere: 59 286 → **60 780 B** (42 chunks/11 ticks) in `test_conn.cpp` `kServedPageBytes` + its loop expectation, `config.h` comments, README; `test_conn` 280→282, `test_page` 142→159, totals 5026→**5045** in README ×2 + AGENTS.md; README Controller-page section gains the responsive paragraph + sticky-ARM row note; `.agents/HW_VERIFICATION.md` Phase 04 rows filled.
|
||||
|
||||
**Tests**: `bash tests/run_tests.sh` → 12 suites, 5045 checks, 0 failures ("RESULT: all suites passed"); `bash .agents/validate.sh` → **validation OK** (both suite runs, bridge/console/rollback builds, page bytes 60 780 quoted==measured, hygiene). Bridge RAM 99 744 B / Flash 1 259 421 B.
|
||||
|
||||
**Docs**: README + AGENTS.md numbers/paragraphs above.
|
||||
|
||||
**Decisions/deviations**: README page-byte/total figures updated now (task 03's scope) because the gate's doc check would otherwise fail. Device legs recorded per the hardware contract: capped `curl` pre-edit returned 51 626/`dd16e195…` (board still on pre-03 firmware); reflash and the 360 px phone observation → `skipped (no device)` / unobserved (live HID keyboard into the build host; no phone headlessly).
|
||||
|
||||
**Next pending task**: `04_controller_page_responsive/02_prose_trim_pinned.md`.
|
||||
+1
@@ -0,0 +1 @@
|
||||
ok
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
== host suites: bash tests/run_tests.sh ==
|
||||
compiler: g++ inside localhost/ai-sandbox:ubuntu-9a08b1d500b0 (repo mounted at /work)
|
||||
keystate 40 checks, 0 failures
|
||||
proto 2379 checks, 0 failures
|
||||
text 459 checks, 0 failures
|
||||
mouse 48 checks, 0 failures
|
||||
output 153 checks, 0 failures
|
||||
ws_codec 135 checks, 0 failures
|
||||
test_tls_pem 862 checks, 0 failures
|
||||
test_tls_session 118 checks, 0 failures
|
||||
test_conn 282 checks, 0 failures
|
||||
test_claim 102 checks, 0 failures
|
||||
handshake
|
||||
frame encoding
|
||||
frame decoding
|
||||
key table
|
||||
pasted text
|
||||
mouse
|
||||
protocol strings
|
||||
the fail-safe wiring
|
||||
the bridge handover (phase 03)
|
||||
one routing path, two transports
|
||||
the client side of pinning
|
||||
the busy board and --take-over
|
||||
the target-side verifier
|
||||
t= 0.00s >>> Ctrl+Alt+Del <<<
|
||||
relayctl 308 checks, 0 failures
|
||||
the log stays readable
|
||||
characters
|
||||
key names in the log
|
||||
sending text
|
||||
refusing what it cannot type
|
||||
stopping
|
||||
the mouse
|
||||
the connect path
|
||||
the channel pill and the certificate line
|
||||
the busy board and the take-over
|
||||
responsive layout
|
||||
element ids
|
||||
page 159 checks, 0 failures
|
||||
RESULT: all suites passed
|
||||
|
||||
== host suites: SANITIZE=1 bash tests/run_tests.sh ==
|
||||
compiler: g++ inside localhost/ai-sandbox:ubuntu-9a08b1d500b0 (repo mounted at /work)
|
||||
sanitizers: on
|
||||
keystate 40 checks, 0 failures
|
||||
proto 2379 checks, 0 failures
|
||||
text 459 checks, 0 failures
|
||||
mouse 48 checks, 0 failures
|
||||
output 153 checks, 0 failures
|
||||
ws_codec 135 checks, 0 failures
|
||||
test_tls_pem 862 checks, 0 failures
|
||||
test_tls_session 118 checks, 0 failures
|
||||
test_conn 282 checks, 0 failures
|
||||
test_claim 102 checks, 0 failures
|
||||
handshake
|
||||
frame encoding
|
||||
frame decoding
|
||||
key table
|
||||
pasted text
|
||||
mouse
|
||||
protocol strings
|
||||
the fail-safe wiring
|
||||
the bridge handover (phase 03)
|
||||
one routing path, two transports
|
||||
the client side of pinning
|
||||
the busy board and --take-over
|
||||
the target-side verifier
|
||||
t= 0.00s >>> Ctrl+Alt+Del <<<
|
||||
relayctl 308 checks, 0 failures
|
||||
the log stays readable
|
||||
characters
|
||||
key names in the log
|
||||
sending text
|
||||
refusing what it cannot type
|
||||
stopping
|
||||
the mouse
|
||||
the connect path
|
||||
the channel pill and the certificate line
|
||||
the busy board and the take-over
|
||||
responsive layout
|
||||
element ids
|
||||
page 159 checks, 0 failures
|
||||
RESULT: all suites passed
|
||||
|
||||
== firmware: platformio run -e bridge -e console -e rollback ==
|
||||
Processing bridge (platform: espressif32@8.0.0; board: esp32-s3-devkitc-1; framework: arduino)
|
||||
--------------------------------------------------------------------------------
|
||||
Verbose mode can be enabled via `-v, --verbose` option
|
||||
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
|
||||
PLATFORM: Espressif 32 (8.0.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
|
||||
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
|
||||
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
|
||||
PACKAGES:
|
||||
- framework-arduinoespressif32 @ 4.30312.261024 (3.3.12)
|
||||
- framework-arduinoespressif32-esp32s3-libs @ 3.3.12
|
||||
- tool-esptool @ 2.50400.0 (5.4.0)
|
||||
- tool-riscv32-esp-elf-gdb @ 12.1.0+20221002
|
||||
- tool-xtensa-esp-elf-gdb @ 12.1.0+20221002
|
||||
- toolchain-riscv32-esp @ 14.2.0+20260121
|
||||
- toolchain-xtensa-esp-elf @ 14.2.0+20260121
|
||||
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
|
||||
LDF Modes: Finder ~ chain, Compatibility ~ soft
|
||||
Found 45 compatible libraries
|
||||
Scanning dependencies...
|
||||
Dependency Graph
|
||||
|-- Adafruit NeoPixel @ 1.15.5
|
||||
|-- ESPmDNS @ 3.3.12
|
||||
|-- USB @ 3.3.12
|
||||
|-- Preferences @ 3.3.12
|
||||
|-- WiFi @ 3.3.12
|
||||
Building in release mode
|
||||
Generating partitions .pio/build/bridge/partitions.bin
|
||||
Retrieving maximum program size .pio/build/bridge/firmware.elf
|
||||
Checking size .pio/build/bridge/firmware.elf
|
||||
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
|
||||
RAM: [=== ] 30.4% (used 99744 bytes from 327680 bytes)
|
||||
Flash: [==== ] 40.0% (used 1259421 bytes from 3145728 bytes)
|
||||
========================= [SUCCESS] Took 0.87 seconds =========================
|
||||
|
||||
Processing console (platform: espressif32@8.0.0; board: esp32-s3-devkitc-1; framework: arduino)
|
||||
--------------------------------------------------------------------------------
|
||||
Verbose mode can be enabled via `-v, --verbose` option
|
||||
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
|
||||
PLATFORM: Espressif 32 (8.0.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
|
||||
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
|
||||
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
|
||||
PACKAGES:
|
||||
- framework-arduinoespressif32 @ 4.30312.261024 (3.3.12)
|
||||
- framework-arduinoespressif32-esp32s3-libs @ 3.3.12
|
||||
- tool-esptool @ 2.50400.0 (5.4.0)
|
||||
- tool-riscv32-esp-elf-gdb @ 12.1.0+20221002
|
||||
- tool-xtensa-esp-elf-gdb @ 12.1.0+20221002
|
||||
- toolchain-riscv32-esp @ 14.2.0+20260121
|
||||
- toolchain-xtensa-esp-elf @ 14.2.0+20260121
|
||||
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
|
||||
LDF Modes: Finder ~ chain, Compatibility ~ soft
|
||||
Found 45 compatible libraries
|
||||
Scanning dependencies...
|
||||
Dependency Graph
|
||||
|-- Adafruit NeoPixel @ 1.15.5
|
||||
|-- ESPmDNS @ 3.3.12
|
||||
|-- USB @ 3.3.12
|
||||
|-- Preferences @ 3.3.12
|
||||
|-- WiFi @ 3.3.12
|
||||
Building in release mode
|
||||
Generating partitions .pio/build/console/partitions.bin
|
||||
Retrieving maximum program size .pio/build/console/firmware.elf
|
||||
Checking size .pio/build/console/firmware.elf
|
||||
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
|
||||
RAM: [== ] 20.6% (used 67352 bytes from 327680 bytes)
|
||||
Flash: [==== ] 37.5% (used 1179661 bytes from 3145728 bytes)
|
||||
========================= [SUCCESS] Took 0.86 seconds =========================
|
||||
|
||||
Processing rollback (platform: espressif32@8.0.0; board: esp32-s3-devkitc-1; framework: arduino)
|
||||
--------------------------------------------------------------------------------
|
||||
Verbose mode can be enabled via `-v, --verbose` option
|
||||
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
|
||||
PLATFORM: Espressif 32 (8.0.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
|
||||
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
|
||||
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
|
||||
PACKAGES:
|
||||
- framework-arduinoespressif32 @ 4.30312.261024 (3.3.12)
|
||||
- framework-arduinoespressif32-esp32s3-libs @ 3.3.12
|
||||
- tool-esptool @ 2.50400.0 (5.4.0)
|
||||
- tool-riscv32-esp-elf-gdb @ 12.1.0+20221002
|
||||
- tool-xtensa-esp-elf-gdb @ 12.1.0+20221002
|
||||
- toolchain-riscv32-esp @ 14.2.0+20260121
|
||||
- toolchain-xtensa-esp-elf @ 14.2.0+20260121
|
||||
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
|
||||
LDF Modes: Finder ~ chain, Compatibility ~ soft
|
||||
Found 45 compatible libraries
|
||||
Scanning dependencies...
|
||||
Dependency Graph
|
||||
|-- Adafruit NeoPixel @ 1.15.5
|
||||
|-- ESPmDNS @ 3.3.12
|
||||
|-- USB @ 3.3.12
|
||||
|-- Preferences @ 3.3.12
|
||||
|-- WiFi @ 3.3.12
|
||||
Building in release mode
|
||||
Generating partitions .pio/build/rollback/partitions.bin
|
||||
Retrieving maximum program size .pio/build/rollback/firmware.elf
|
||||
Checking size .pio/build/rollback/firmware.elf
|
||||
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
|
||||
RAM: [=== ] 28.6% (used 93568 bytes from 327680 bytes)
|
||||
Flash: [==== ] 36.8% (used 1159113 bytes from 3145728 bytes)
|
||||
========================= [SUCCESS] Took 0.87 seconds =========================
|
||||
|
||||
Environment Status Duration
|
||||
------------- -------- ------------
|
||||
bridge SUCCESS 00:00:00.870
|
||||
console SUCCESS 00:00:00.859
|
||||
rollback SUCCESS 00:00:00.870
|
||||
========================= 3 succeeded in 00:00:02.599 =========================
|
||||
|
||||
== page size: src/web_page.h raw strings ==
|
||||
page bytes: 60780 (a reflashed device must serve exactly this many; kIndexHtmlLen is sizeof(kIndexHtml)-1 of these same bytes, both builds)
|
||||
|
||||
== README/AGENTS claims vs what this run measured ==
|
||||
README/AGENTS claims: OK (5 045 checks quoted and measured in both runs; 12 Tests-table rows match their suites; 60 780 page bytes quoted; Backlog contiguous; every ws:// sentence names the rollback)
|
||||
|
||||
== repo hygiene ==
|
||||
repo hygiene: OK (131 tracked files, no ignored-tracked files, no key-material shapes, no PII hits)
|
||||
|
||||
== hardware context (informational; cannot change the gate result) ==
|
||||
hw-probe serial /dev/ttyACM0 present, readable (NOT opened: opening toggles DTR/RTS and resets the board)
|
||||
hw-probe mdns keyrelay.local resolves -> 10.2.0.22
|
||||
hw-probe https https://keyrelay.local/ -> HTTP 200 (TLS listener is up)
|
||||
hw-probe http http://keyrelay.local/ -> HTTP 301, redirects to https://keyrelay.local/ (redirect-only: the post-phase-02 shape)
|
||||
hw-probe tls 10.2.0.22:443 negotiated TLSv1.2 / ECDHE-ECDSA-AES256-GCM-SHA384 (a self-signed verify failure is the expected shape; the fingerprint to compare it against is on the serial console)
|
||||
hw-probe uv /home/ducoterra/.local/bin/uv — firmware builds available
|
||||
hw-probe node /usr/bin/node — test_page.js runs
|
||||
hw-probe evdev board 046d:c33f visible: keyboard=/dev/input/event19 (readable) mouse=/dev/input/event20 (readable) — python3 tools/watch_target.py runs ON THIS machine (the target)
|
||||
validation OK (suites + sanitised suites + bridge/console/rollback builds + page size
|
||||
+ the numbers README/AGENTS quote + hygiene)
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
**Task 02 — prose trim, pinned: done.**
|
||||
|
||||
- **Assertions first** (`tests/test_page.js`, 159→**184**): 21 new safety-sentence pins matched as *text* out of the served markup (tags stripped, entities decoded, case/whitespace folded, so a sentence survives a rewrap and a move into `<details>` but fails when deleted) — every-key/including-Enter + which machine + permission rule, banner before every fold, nothing-forwarded-while-disarmed + what disarms, `Esc` frees cursor *and* releases buttons + never reaches target + button restatement, stop-typing abandons the run, characters-are-positions/US layout, no-history/nothing-saved/nothing-scheduled, takeover **cannot arm — every handover lands disarmed**, clear-page warning + its fix, CAD VT-reboot, `dflt:1`-driven shout (plus "no static default-passphrase text in markup"); +2 folded-structure pins (`kv` wraps, Log option row) and `TEXT_FRAME_MAX`/`TEXT_JOB_MAX` re-pinned to `config.h`.
|
||||
- **Fold** (`src/web_page.h`): 5 `<details>` (chords/KDE, passphrase, forwarded mouse, paste run, footer shortcuts), one-line summaries left visible, arming banner untouched, multi-sentence `title` tooltips shortened, inline styles → `.mt/.dim/.hot/.dimv`. Visible prose **2 866 → 862 B (~30 %)**; `#banner` byte-identical, all 11 `data-chord` values and every script-reached id intact, HTML balance checked.
|
||||
- **Pins proven to bite**: 26 mutations (delete/soften each sentence, move the banner behind a fold, drift both constants) each fired its own check; file restored byte-for-byte.
|
||||
- **Tests**: `node tests/test_page.js` 184/0 — run **before** and **after** the trim; `bash tests/run_tests.sh` and `SANITIZE=1 …` → *all suites passed* (5 068 each); `bash .agents/validate.sh` → **validation OK** (bridge/console/rollback SUCCESS; page bytes 59 223 quoted==measured; hygiene OK). `ruff` is not configured or gated in this repo; its 29 findings are all pre-existing in untouched `tools/*.py`/`tests/test_relayctl.py` — left alone.
|
||||
- **Docs**: README Controller-page section gains the fold + "pinned by `tests/test_page.js`" paragraph and a table row; re-pinned page size 60 780→**59 223** (README ×2, `test_conn.cpp` `kServedPageBytes` + 41 chunks/11 ticks, `config.h`), footprint flash pair −64 B, Tests rows `test_page` 184 / `test_conn` 280, totals **5 068** in README ×2 and AGENTS.md. AGENTS.md: only the stale check count (per its no-expansion rule).
|
||||
- **Decisions/deviations**: prose is only 2.5 KB of a 60 KB page and the fold *moves* text rather than deleting it, so meeting "page smaller in bytes" also needed the four longest `<script>` comment blocks and the section-divider rules tightened — every rule/why kept, README stays the durable record. Result 59 223 B: −1 557 vs this task's start, and below the 59 286 B the phase opened at. Device: one capped read-only `curl -sk https://keyrelay.local/` → 51 626 / `dd16e195…` (board still pre-phase-03); reflash and the 360 px phone observation recorded `skipped (no device)` in `.agents/HW_VERIFICATION.md`.
|
||||
- **Next pending task**: `04_controller_page_responsive/03_suite_size_and_docs.md`.
|
||||
+1
@@ -0,0 +1 @@
|
||||
ok
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
== host suites: bash tests/run_tests.sh ==
|
||||
compiler: g++ inside localhost/ai-sandbox:ubuntu-9a08b1d500b0 (repo mounted at /work)
|
||||
keystate 40 checks, 0 failures
|
||||
proto 2379 checks, 0 failures
|
||||
text 459 checks, 0 failures
|
||||
mouse 48 checks, 0 failures
|
||||
output 153 checks, 0 failures
|
||||
ws_codec 135 checks, 0 failures
|
||||
test_tls_pem 862 checks, 0 failures
|
||||
test_tls_session 118 checks, 0 failures
|
||||
test_conn 280 checks, 0 failures
|
||||
test_claim 102 checks, 0 failures
|
||||
handshake
|
||||
frame encoding
|
||||
frame decoding
|
||||
key table
|
||||
pasted text
|
||||
mouse
|
||||
protocol strings
|
||||
the fail-safe wiring
|
||||
the bridge handover (phase 03)
|
||||
one routing path, two transports
|
||||
the client side of pinning
|
||||
the busy board and --take-over
|
||||
the target-side verifier
|
||||
t= 0.00s >>> Ctrl+Alt+Del <<<
|
||||
relayctl 308 checks, 0 failures
|
||||
the log stays readable
|
||||
characters
|
||||
key names in the log
|
||||
sending text
|
||||
refusing what it cannot type
|
||||
stopping
|
||||
the mouse
|
||||
the connect path
|
||||
the channel pill and the certificate line
|
||||
the busy board and the take-over
|
||||
responsive layout
|
||||
the safety statements that must never be tidied away
|
||||
the page's constants are the board's constants
|
||||
element ids
|
||||
page 184 checks, 0 failures
|
||||
RESULT: all suites passed
|
||||
|
||||
== host suites: SANITIZE=1 bash tests/run_tests.sh ==
|
||||
compiler: g++ inside localhost/ai-sandbox:ubuntu-9a08b1d500b0 (repo mounted at /work)
|
||||
sanitizers: on
|
||||
keystate 40 checks, 0 failures
|
||||
proto 2379 checks, 0 failures
|
||||
text 459 checks, 0 failures
|
||||
mouse 48 checks, 0 failures
|
||||
output 153 checks, 0 failures
|
||||
ws_codec 135 checks, 0 failures
|
||||
test_tls_pem 862 checks, 0 failures
|
||||
test_tls_session 118 checks, 0 failures
|
||||
test_conn 280 checks, 0 failures
|
||||
test_claim 102 checks, 0 failures
|
||||
handshake
|
||||
frame encoding
|
||||
frame decoding
|
||||
key table
|
||||
pasted text
|
||||
mouse
|
||||
protocol strings
|
||||
the fail-safe wiring
|
||||
the bridge handover (phase 03)
|
||||
one routing path, two transports
|
||||
the client side of pinning
|
||||
the busy board and --take-over
|
||||
the target-side verifier
|
||||
t= 0.00s >>> Ctrl+Alt+Del <<<
|
||||
relayctl 308 checks, 0 failures
|
||||
the log stays readable
|
||||
characters
|
||||
key names in the log
|
||||
sending text
|
||||
refusing what it cannot type
|
||||
stopping
|
||||
the mouse
|
||||
the connect path
|
||||
the channel pill and the certificate line
|
||||
the busy board and the take-over
|
||||
responsive layout
|
||||
the safety statements that must never be tidied away
|
||||
the page's constants are the board's constants
|
||||
element ids
|
||||
page 184 checks, 0 failures
|
||||
RESULT: all suites passed
|
||||
|
||||
== firmware: platformio run -e bridge -e console -e rollback ==
|
||||
Processing bridge (platform: espressif32@8.0.0; board: esp32-s3-devkitc-1; framework: arduino)
|
||||
--------------------------------------------------------------------------------
|
||||
Verbose mode can be enabled via `-v, --verbose` option
|
||||
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
|
||||
PLATFORM: Espressif 32 (8.0.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
|
||||
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
|
||||
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
|
||||
PACKAGES:
|
||||
- framework-arduinoespressif32 @ 4.30312.261024 (3.3.12)
|
||||
- framework-arduinoespressif32-esp32s3-libs @ 3.3.12
|
||||
- tool-esptool @ 2.50400.0 (5.4.0)
|
||||
- tool-riscv32-esp-elf-gdb @ 12.1.0+20221002
|
||||
- tool-xtensa-esp-elf-gdb @ 12.1.0+20221002
|
||||
- toolchain-riscv32-esp @ 14.2.0+20260121
|
||||
- toolchain-xtensa-esp-elf @ 14.2.0+20260121
|
||||
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
|
||||
LDF Modes: Finder ~ chain, Compatibility ~ soft
|
||||
Found 45 compatible libraries
|
||||
Scanning dependencies...
|
||||
Dependency Graph
|
||||
|-- Adafruit NeoPixel @ 1.15.5
|
||||
|-- ESPmDNS @ 3.3.12
|
||||
|-- USB @ 3.3.12
|
||||
|-- Preferences @ 3.3.12
|
||||
|-- WiFi @ 3.3.12
|
||||
Building in release mode
|
||||
Generating partitions .pio/build/bridge/partitions.bin
|
||||
Retrieving maximum program size .pio/build/bridge/firmware.elf
|
||||
Checking size .pio/build/bridge/firmware.elf
|
||||
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
|
||||
RAM: [=== ] 30.4% (used 99744 bytes from 327680 bytes)
|
||||
Flash: [==== ] 40.0% (used 1257869 bytes from 3145728 bytes)
|
||||
========================= [SUCCESS] Took 0.91 seconds =========================
|
||||
|
||||
Processing console (platform: espressif32@8.0.0; board: esp32-s3-devkitc-1; framework: arduino)
|
||||
--------------------------------------------------------------------------------
|
||||
Verbose mode can be enabled via `-v, --verbose` option
|
||||
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
|
||||
PLATFORM: Espressif 32 (8.0.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
|
||||
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
|
||||
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
|
||||
PACKAGES:
|
||||
- framework-arduinoespressif32 @ 4.30312.261024 (3.3.12)
|
||||
- framework-arduinoespressif32-esp32s3-libs @ 3.3.12
|
||||
- tool-esptool @ 2.50400.0 (5.4.0)
|
||||
- tool-riscv32-esp-elf-gdb @ 12.1.0+20221002
|
||||
- tool-xtensa-esp-elf-gdb @ 12.1.0+20221002
|
||||
- toolchain-riscv32-esp @ 14.2.0+20260121
|
||||
- toolchain-xtensa-esp-elf @ 14.2.0+20260121
|
||||
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
|
||||
LDF Modes: Finder ~ chain, Compatibility ~ soft
|
||||
Found 45 compatible libraries
|
||||
Scanning dependencies...
|
||||
Dependency Graph
|
||||
|-- Adafruit NeoPixel @ 1.15.5
|
||||
|-- ESPmDNS @ 3.3.12
|
||||
|-- USB @ 3.3.12
|
||||
|-- Preferences @ 3.3.12
|
||||
|-- WiFi @ 3.3.12
|
||||
Building in release mode
|
||||
Generating partitions .pio/build/console/partitions.bin
|
||||
Retrieving maximum program size .pio/build/console/firmware.elf
|
||||
Checking size .pio/build/console/firmware.elf
|
||||
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
|
||||
RAM: [== ] 20.6% (used 67352 bytes from 327680 bytes)
|
||||
Flash: [==== ] 37.5% (used 1178109 bytes from 3145728 bytes)
|
||||
========================= [SUCCESS] Took 0.86 seconds =========================
|
||||
|
||||
Processing rollback (platform: espressif32@8.0.0; board: esp32-s3-devkitc-1; framework: arduino)
|
||||
--------------------------------------------------------------------------------
|
||||
Verbose mode can be enabled via `-v, --verbose` option
|
||||
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
|
||||
PLATFORM: Espressif 32 (8.0.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
|
||||
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
|
||||
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
|
||||
PACKAGES:
|
||||
- framework-arduinoespressif32 @ 4.30312.261024 (3.3.12)
|
||||
- framework-arduinoespressif32-esp32s3-libs @ 3.3.12
|
||||
- tool-esptool @ 2.50400.0 (5.4.0)
|
||||
- tool-riscv32-esp-elf-gdb @ 12.1.0+20221002
|
||||
- tool-xtensa-esp-elf-gdb @ 12.1.0+20221002
|
||||
- toolchain-riscv32-esp @ 14.2.0+20260121
|
||||
- toolchain-xtensa-esp-elf @ 14.2.0+20260121
|
||||
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
|
||||
LDF Modes: Finder ~ chain, Compatibility ~ soft
|
||||
Found 45 compatible libraries
|
||||
Scanning dependencies...
|
||||
Dependency Graph
|
||||
|-- Adafruit NeoPixel @ 1.15.5
|
||||
|-- ESPmDNS @ 3.3.12
|
||||
|-- USB @ 3.3.12
|
||||
|-- Preferences @ 3.3.12
|
||||
|-- WiFi @ 3.3.12
|
||||
Building in release mode
|
||||
Generating partitions .pio/build/rollback/partitions.bin
|
||||
Retrieving maximum program size .pio/build/rollback/firmware.elf
|
||||
Checking size .pio/build/rollback/firmware.elf
|
||||
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
|
||||
RAM: [=== ] 28.6% (used 93568 bytes from 327680 bytes)
|
||||
Flash: [==== ] 36.8% (used 1157545 bytes from 3145728 bytes)
|
||||
========================= [SUCCESS] Took 0.85 seconds =========================
|
||||
|
||||
Environment Status Duration
|
||||
------------- -------- ------------
|
||||
bridge SUCCESS 00:00:00.910
|
||||
console SUCCESS 00:00:00.858
|
||||
rollback SUCCESS 00:00:00.849
|
||||
========================= 3 succeeded in 00:00:02.617 =========================
|
||||
|
||||
== page size: src/web_page.h raw strings ==
|
||||
page bytes: 59223 (a reflashed device must serve exactly this many; kIndexHtmlLen is sizeof(kIndexHtml)-1 of these same bytes, both builds)
|
||||
|
||||
== README/AGENTS claims vs what this run measured ==
|
||||
README/AGENTS claims: OK (5 068 checks quoted and measured in both runs; 12 Tests-table rows match their suites; 59 223 page bytes quoted; Backlog contiguous; every ws:// sentence names the rollback)
|
||||
|
||||
== repo hygiene ==
|
||||
repo hygiene: OK (131 tracked files, no ignored-tracked files, no key-material shapes, no PII hits)
|
||||
|
||||
== hardware context (informational; cannot change the gate result) ==
|
||||
hw-probe serial /dev/ttyACM0 present, readable (NOT opened: opening toggles DTR/RTS and resets the board)
|
||||
hw-probe mdns keyrelay.local resolves -> 10.2.0.22
|
||||
hw-probe https https://keyrelay.local/ -> HTTP 200 (TLS listener is up)
|
||||
hw-probe http http://keyrelay.local/ -> HTTP 301, redirects to https://keyrelay.local/ (redirect-only: the post-phase-02 shape)
|
||||
hw-probe tls 10.2.0.22:443 negotiated TLSv1.2 / ECDHE-ECDSA-AES256-GCM-SHA384 (a self-signed verify failure is the expected shape; the fingerprint to compare it against is on the serial console)
|
||||
hw-probe uv /home/ducoterra/.local/bin/uv — firmware builds available
|
||||
hw-probe node /usr/bin/node — test_page.js runs
|
||||
hw-probe evdev board 046d:c33f visible: keyboard=/dev/input/event19 (readable) mouse=/dev/input/event20 (readable) — python3 tools/watch_target.py runs ON THIS machine (the target)
|
||||
validation OK (suites + sanitised suites + bridge/console/rollback builds + page size
|
||||
+ the numbers README/AGENTS quote + hygiene)
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
**Task 03 — suite coverage, page size, README: done.**
|
||||
|
||||
- **`tests/test_page.js` 184→219**: new sections — arming & `S=` state frames on the wire (un-linked ARM click sends nothing; `A1` alone, `A0`+`R` in order, no optimistic flip either way, board-status arms/mirrors; disarmed press forwards nothing; whole-state restatements `S=00:04`→`S=02:05`→`S=00:`, autorepeat silent, page-field keys stay local but a modifier released *above* a field is let go; `Ctrl+Alt+Esc` = `R,A0`; every emitted frame matches the parser's wire form); folded-note structure (exactly 5 `<details>`, each with a naming summary + real sentences — folded ≠ deleted); id-coverage self-test proving the markup-vs-shim comparison names a fabricated id; `vm.runInContext` now tags `web_page.js` (stack traces + V8 coverage); stale "no CSS, no layout" header comment corrected.
|
||||
- **Every new check proven to bite**: 10 mutations of `web_page.h` (delete/hollow/strip a fold, optimistic flip, repeat leak, stranded Shift, `R`-without-`A0`, inverted `A0,R`, broken wire form, config and page-copy constant drift) each fired its check; both files restored byte-for-byte (`sha256sum -c` OK). No `src/*` change from this task.
|
||||
- **Suite/gate** (captured output): `bash tests/run_tests.sh` and `SANITIZE=1 …` → 12 suites, **5103 checks, 0 failures** each; `bash .agents/validate.sh` → **validation OK** (bridge/console/rollback SUCCESS; `page bytes: 59223` quoted==measured; docs claims OK; hygiene OK). V8 coverage of the page script: 86 % of code chars executed, 73/96 functions entered.
|
||||
- **README**: one `test_page` row at **219** (no duplicate; the 47-check row is gone), intro + Quick-start totals → **5103**, Controller-page section now names the measurement (`curl -sk https://keyrelay.local/ | wc -c` vs the gate's `page bytes:` line, device leg recorded in HW_VERIFICATION). Quick start/Troubleshooting checked — no old-layout statements left. **AGENTS.md**: only the stale check count (per its no-expansion rule).
|
||||
- **HW_VERIFICATION.md**: one capped probe ran — `curl -sk https://keyrelay.local/ | wc -c` → 51 626 / `dd16e195…` (board still pre-phase-03; tree's assembled page 59 223 / `dc26b441…b634`); phone row stays `skipped (no device)` with the 360 px / folded-notes-expandable checklist and commands. Byte-equality closes on the next flash (standing no-unattended-reflash call, as tasks 01–02 recorded).
|
||||
- **Next pending task**: `05_mobile_controller` (this phase's task file moves to complete/ by the harness).
|
||||
+1
@@ -0,0 +1 @@
|
||||
ok
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
== host suites: bash tests/run_tests.sh ==
|
||||
compiler: g++ inside localhost/ai-sandbox:ubuntu-9a08b1d500b0 (repo mounted at /work)
|
||||
keystate 40 checks, 0 failures
|
||||
proto 2379 checks, 0 failures
|
||||
text 459 checks, 0 failures
|
||||
mouse 48 checks, 0 failures
|
||||
output 153 checks, 0 failures
|
||||
ws_codec 135 checks, 0 failures
|
||||
test_tls_pem 862 checks, 0 failures
|
||||
test_tls_session 118 checks, 0 failures
|
||||
test_conn 280 checks, 0 failures
|
||||
test_claim 102 checks, 0 failures
|
||||
handshake
|
||||
frame encoding
|
||||
frame decoding
|
||||
key table
|
||||
pasted text
|
||||
mouse
|
||||
protocol strings
|
||||
the fail-safe wiring
|
||||
the bridge handover (phase 03)
|
||||
one routing path, two transports
|
||||
the client side of pinning
|
||||
the busy board and --take-over
|
||||
the target-side verifier
|
||||
t= 0.00s >>> Ctrl+Alt+Del <<<
|
||||
relayctl 308 checks, 0 failures
|
||||
the log stays readable
|
||||
characters
|
||||
key names in the log
|
||||
sending text
|
||||
refusing what it cannot type
|
||||
stopping
|
||||
the mouse
|
||||
the connect path
|
||||
the channel pill and the certificate line
|
||||
the busy board and the take-over
|
||||
arming and the S= state frames
|
||||
responsive layout
|
||||
the safety statements that must never be tidied away
|
||||
the folded notes are folded, not deleted
|
||||
the page's constants are the board's constants
|
||||
element ids
|
||||
page 219 checks, 0 failures
|
||||
RESULT: all suites passed
|
||||
|
||||
== host suites: SANITIZE=1 bash tests/run_tests.sh ==
|
||||
compiler: g++ inside localhost/ai-sandbox:ubuntu-9a08b1d500b0 (repo mounted at /work)
|
||||
sanitizers: on
|
||||
keystate 40 checks, 0 failures
|
||||
proto 2379 checks, 0 failures
|
||||
text 459 checks, 0 failures
|
||||
mouse 48 checks, 0 failures
|
||||
output 153 checks, 0 failures
|
||||
ws_codec 135 checks, 0 failures
|
||||
test_tls_pem 862 checks, 0 failures
|
||||
test_tls_session 118 checks, 0 failures
|
||||
test_conn 280 checks, 0 failures
|
||||
test_claim 102 checks, 0 failures
|
||||
handshake
|
||||
frame encoding
|
||||
frame decoding
|
||||
key table
|
||||
pasted text
|
||||
mouse
|
||||
protocol strings
|
||||
the fail-safe wiring
|
||||
the bridge handover (phase 03)
|
||||
one routing path, two transports
|
||||
the client side of pinning
|
||||
the busy board and --take-over
|
||||
the target-side verifier
|
||||
t= 0.00s >>> Ctrl+Alt+Del <<<
|
||||
relayctl 308 checks, 0 failures
|
||||
the log stays readable
|
||||
characters
|
||||
key names in the log
|
||||
sending text
|
||||
refusing what it cannot type
|
||||
stopping
|
||||
the mouse
|
||||
the connect path
|
||||
the channel pill and the certificate line
|
||||
the busy board and the take-over
|
||||
arming and the S= state frames
|
||||
responsive layout
|
||||
the safety statements that must never be tidied away
|
||||
the folded notes are folded, not deleted
|
||||
the page's constants are the board's constants
|
||||
element ids
|
||||
page 219 checks, 0 failures
|
||||
RESULT: all suites passed
|
||||
|
||||
== firmware: platformio run -e bridge -e console -e rollback ==
|
||||
Processing bridge (platform: espressif32@8.0.0; board: esp32-s3-devkitc-1; framework: arduino)
|
||||
--------------------------------------------------------------------------------
|
||||
Verbose mode can be enabled via `-v, --verbose` option
|
||||
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
|
||||
PLATFORM: Espressif 32 (8.0.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
|
||||
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
|
||||
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
|
||||
PACKAGES:
|
||||
- framework-arduinoespressif32 @ 4.30312.261024 (3.3.12)
|
||||
- framework-arduinoespressif32-esp32s3-libs @ 3.3.12
|
||||
- tool-esptool @ 2.50400.0 (5.4.0)
|
||||
- tool-riscv32-esp-elf-gdb @ 12.1.0+20221002
|
||||
- tool-xtensa-esp-elf-gdb @ 12.1.0+20221002
|
||||
- toolchain-riscv32-esp @ 14.2.0+20260121
|
||||
- toolchain-xtensa-esp-elf @ 14.2.0+20260121
|
||||
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
|
||||
LDF Modes: Finder ~ chain, Compatibility ~ soft
|
||||
Found 45 compatible libraries
|
||||
Scanning dependencies...
|
||||
Dependency Graph
|
||||
|-- Adafruit NeoPixel @ 1.15.5
|
||||
|-- ESPmDNS @ 3.3.12
|
||||
|-- USB @ 3.3.12
|
||||
|-- Preferences @ 3.3.12
|
||||
|-- WiFi @ 3.3.12
|
||||
Building in release mode
|
||||
Generating partitions .pio/build/bridge/partitions.bin
|
||||
Retrieving maximum program size .pio/build/bridge/firmware.elf
|
||||
Checking size .pio/build/bridge/firmware.elf
|
||||
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
|
||||
RAM: [=== ] 30.4% (used 99744 bytes from 327680 bytes)
|
||||
Flash: [==== ] 40.0% (used 1257869 bytes from 3145728 bytes)
|
||||
========================= [SUCCESS] Took 0.80 seconds =========================
|
||||
|
||||
Processing console (platform: espressif32@8.0.0; board: esp32-s3-devkitc-1; framework: arduino)
|
||||
--------------------------------------------------------------------------------
|
||||
Verbose mode can be enabled via `-v, --verbose` option
|
||||
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
|
||||
PLATFORM: Espressif 32 (8.0.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
|
||||
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
|
||||
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
|
||||
PACKAGES:
|
||||
- framework-arduinoespressif32 @ 4.30312.261024 (3.3.12)
|
||||
- framework-arduinoespressif32-esp32s3-libs @ 3.3.12
|
||||
- tool-esptool @ 2.50400.0 (5.4.0)
|
||||
- tool-riscv32-esp-elf-gdb @ 12.1.0+20221002
|
||||
- tool-xtensa-esp-elf-gdb @ 12.1.0+20221002
|
||||
- toolchain-riscv32-esp @ 14.2.0+20260121
|
||||
- toolchain-xtensa-esp-elf @ 14.2.0+20260121
|
||||
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
|
||||
LDF Modes: Finder ~ chain, Compatibility ~ soft
|
||||
Found 45 compatible libraries
|
||||
Scanning dependencies...
|
||||
Dependency Graph
|
||||
|-- Adafruit NeoPixel @ 1.15.5
|
||||
|-- ESPmDNS @ 3.3.12
|
||||
|-- USB @ 3.3.12
|
||||
|-- Preferences @ 3.3.12
|
||||
|-- WiFi @ 3.3.12
|
||||
Building in release mode
|
||||
Generating partitions .pio/build/console/partitions.bin
|
||||
Retrieving maximum program size .pio/build/console/firmware.elf
|
||||
Checking size .pio/build/console/firmware.elf
|
||||
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
|
||||
RAM: [== ] 20.6% (used 67352 bytes from 327680 bytes)
|
||||
Flash: [==== ] 37.5% (used 1178109 bytes from 3145728 bytes)
|
||||
========================= [SUCCESS] Took 0.79 seconds =========================
|
||||
|
||||
Processing rollback (platform: espressif32@8.0.0; board: esp32-s3-devkitc-1; framework: arduino)
|
||||
--------------------------------------------------------------------------------
|
||||
Verbose mode can be enabled via `-v, --verbose` option
|
||||
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
|
||||
PLATFORM: Espressif 32 (8.0.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
|
||||
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
|
||||
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
|
||||
PACKAGES:
|
||||
- framework-arduinoespressif32 @ 4.30312.261024 (3.3.12)
|
||||
- framework-arduinoespressif32-esp32s3-libs @ 3.3.12
|
||||
- tool-esptool @ 2.50400.0 (5.4.0)
|
||||
- tool-riscv32-esp-elf-gdb @ 12.1.0+20221002
|
||||
- tool-xtensa-esp-elf-gdb @ 12.1.0+20221002
|
||||
- toolchain-riscv32-esp @ 14.2.0+20260121
|
||||
- toolchain-xtensa-esp-elf @ 14.2.0+20260121
|
||||
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
|
||||
LDF Modes: Finder ~ chain, Compatibility ~ soft
|
||||
Found 45 compatible libraries
|
||||
Scanning dependencies...
|
||||
Dependency Graph
|
||||
|-- Adafruit NeoPixel @ 1.15.5
|
||||
|-- ESPmDNS @ 3.3.12
|
||||
|-- USB @ 3.3.12
|
||||
|-- Preferences @ 3.3.12
|
||||
|-- WiFi @ 3.3.12
|
||||
Building in release mode
|
||||
Generating partitions .pio/build/rollback/partitions.bin
|
||||
Retrieving maximum program size .pio/build/rollback/firmware.elf
|
||||
Checking size .pio/build/rollback/firmware.elf
|
||||
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
|
||||
RAM: [=== ] 28.6% (used 93568 bytes from 327680 bytes)
|
||||
Flash: [==== ] 36.8% (used 1157545 bytes from 3145728 bytes)
|
||||
========================= [SUCCESS] Took 0.79 seconds =========================
|
||||
|
||||
Environment Status Duration
|
||||
------------- -------- ------------
|
||||
bridge SUCCESS 00:00:00.798
|
||||
console SUCCESS 00:00:00.792
|
||||
rollback SUCCESS 00:00:00.790
|
||||
========================= 3 succeeded in 00:00:02.380 =========================
|
||||
|
||||
== page size: src/web_page.h raw strings ==
|
||||
page bytes: 59223 (a reflashed device must serve exactly this many; kIndexHtmlLen is sizeof(kIndexHtml)-1 of these same bytes, both builds)
|
||||
|
||||
== README/AGENTS claims vs what this run measured ==
|
||||
README/AGENTS claims: OK (5 103 checks quoted and measured in both runs; 12 Tests-table rows match their suites; 59 223 page bytes quoted; Backlog contiguous; every ws:// sentence names the rollback)
|
||||
|
||||
== repo hygiene ==
|
||||
repo hygiene: OK (131 tracked files, no ignored-tracked files, no key-material shapes, no PII hits)
|
||||
|
||||
== hardware context (informational; cannot change the gate result) ==
|
||||
hw-probe serial /dev/ttyACM0 present, readable (NOT opened: opening toggles DTR/RTS and resets the board)
|
||||
hw-probe mdns keyrelay.local resolves -> 10.2.0.22
|
||||
hw-probe https https://keyrelay.local/ -> HTTP 200 (TLS listener is up)
|
||||
hw-probe http http://keyrelay.local/ -> HTTP 301, redirects to https://keyrelay.local/ (redirect-only: the post-phase-02 shape)
|
||||
hw-probe tls 10.2.0.22:443 negotiated TLSv1.2 / ECDHE-ECDSA-AES256-GCM-SHA384 (a self-signed verify failure is the expected shape; the fingerprint to compare it against is on the serial console)
|
||||
hw-probe uv /home/ducoterra/.local/bin/uv — firmware builds available
|
||||
hw-probe node /usr/bin/node — test_page.js runs
|
||||
hw-probe evdev board 046d:c33f visible: keyboard=/dev/input/event19 (readable) mouse=/dev/input/event20 (readable) — python3 tools/watch_target.py runs ON THIS machine (the target)
|
||||
validation OK (suites + sanitised suites + bridge/console/rollback builds + page size
|
||||
+ the numbers README/AGENTS quote + hygiene)
|
||||
@@ -10,7 +10,7 @@ The README is the single source of truth for behaviour, protocol, safety and mea
|
||||
This file is the operating manual: what must not break, how to build and verify, and where
|
||||
the traps are. **Do not fork the protocol or the safety model into `docs/`** — a second copy
|
||||
drifts, and two paths that can disagree about arming is the exact failure this project keeps
|
||||
having to fix (see README, "Thirteen bugs").
|
||||
having to fix (see README, "Fourteen bugs").
|
||||
|
||||
## Phased execution
|
||||
|
||||
@@ -98,7 +98,7 @@ the page and `TEXT_FRAME_MAX` / `MAX_PASTE_CHARS` in `relayctl.py` are hand-copi
|
||||
## Build, test, flash
|
||||
|
||||
```bash
|
||||
tests/run_tests.sh # 5026 checks, no hardware
|
||||
tests/run_tests.sh # 5104 checks, no hardware
|
||||
SANITIZE=1 tests/run_tests.sh # + ASan/UBSan — use it for anything touching buffers
|
||||
# PlatformIO. The venv below runs Python 3.15, which esptool REFUSES ("Python version must
|
||||
# be 3.10..3.14") -- builds fine, dies at upload. Use uv's 3.14 for anything with -t upload:
|
||||
@@ -202,7 +202,7 @@ wired now — but ask, of every green check, *what would still pass if this were
|
||||
nothing may print it, hash-and-print it in a way that reads as the key, or be added to the
|
||||
tree as a `.pem`. Fingerprint yes, key never.
|
||||
- `TODO.md` holds **open items only**; clear it when they are done. The durable record is the
|
||||
README: a fixed bug belongs in "Thirteen bugs this verification caught" with its root cause and
|
||||
README: a fixed bug belongs in "Fourteen bugs this verification caught" with its root cause and
|
||||
how it was proven, a new capability in the section that describes it, and a rule worth
|
||||
repeating here. A TODO that has accumulated resolved items tells the next reader nothing about
|
||||
what is left to do.
|
||||
|
||||
+3
-3
@@ -112,7 +112,7 @@ constexpr uint32_t kTlsHandshakeMs = 10000;
|
||||
// 64 KB of heap at the full two sessions. mbedtls_ssl_conf_max_frag_len(4096) was NOT
|
||||
// applied: the measured free heap with two live sessions (see .agents/HW_VERIFICATION.md,
|
||||
// phase 02 task 03) fits with margin, so the smaller-fragments saving is not worth the
|
||||
// smaller record ceiling on the 48 KB page. Re-measure before changing that answer.
|
||||
// smaller record ceiling on the ~58 KB page. Re-measure before changing that answer.
|
||||
constexpr size_t kTlsMaxSessions = 2;
|
||||
|
||||
// Bytes staged between the server's writes and a transport that may not accept one this
|
||||
@@ -139,11 +139,11 @@ constexpr size_t kConnWriteChunk = 4096;
|
||||
// write. 1460 = one TCP MSS (1500 MTU - 20 IP - 20 TCP), so a chunk leaves as exactly one
|
||||
// segment and exactly one TLS record, and it is below kConnWriteChunk by construction. The
|
||||
// cost is static RAM: one buffer per live conn, 2 x 1460 = 2920 B, quoted in the phase
|
||||
// report against the phase 01 baseline. The served page is 59 286 B (the count the gate
|
||||
// report against the phase 01 baseline. The served page is 59 262 B (the count the gate
|
||||
// prints), so a page is 41 chunks.
|
||||
constexpr size_t kPageChunkBytes = 1460;
|
||||
|
||||
// The per-tick budget for draining a slot's queue: 4 chunks, so the 59 286-byte page leaves
|
||||
// The per-tick budget for draining a slot's queue: 4 chunks, so the 59 262-byte page leaves
|
||||
// in 11 ticks of pump() and no single loop() iteration can spend more than four transport
|
||||
// writes on one socket -- whatever that socket's peer is doing. loop() runs continuously
|
||||
// (nothing in it delays), so the page is out in milliseconds; the budget is not about
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@
|
||||
* - TLS hands `mbedtls_ssl_write` the identical (ptr, len) until it accepts them, which
|
||||
* is that library's documented resume rule and is enforced in tls::Session;
|
||||
* - both are capped per call (`kConnWriteChunk`) and per tick (`kPageBytesPerTick`), and
|
||||
* a body that lives in flash is copied through a bounded RAM chunk, so a 48 KB page
|
||||
* a body that lives in flash is copied through a bounded RAM chunk, so a ~58 KB page
|
||||
* leaves in bounded pieces across ticks instead of in one call that can wait.
|
||||
* Nothing here ever waits. A peer that stops reading is dropped by the caller's stall
|
||||
* deadline, which is the same rule that already applied to a stalled *request*.
|
||||
|
||||
@@ -507,7 +507,7 @@ void ControlServer::serveReadonly(const ws::Request &req, BuiltResponse &rsp,
|
||||
if (strcmp(req.path, kPathIndex) == 0 || strcmp(req.path, kPathIndexHtml) == 0) {
|
||||
// The page is the one body too big to queue: it is streamed out of flash through a
|
||||
// bounded chunk (config.h kPageChunkBytes / kPageBytesPerTick), which is what lets a
|
||||
// 48 KB response leave across ticks on either transport without one blocking write.
|
||||
// ~58 KB response leave across ticks on either transport without one blocking write.
|
||||
buildResponse(rsp, "200 OK", "text/html; charset=utf-8", BuiltResponse::Body::Text,
|
||||
kIndexHtml, kIndexHtmlLen, nullptr, /*textInFlash=*/true);
|
||||
return;
|
||||
|
||||
@@ -161,7 +161,7 @@ class ControlServer {
|
||||
|
||||
// A response built but not yet written: the status line and headers, plus a body that
|
||||
// lives somewhere else -- a string literal in flash, the rendered status JSON, or the
|
||||
// 48 KB page in PROGMEM. Every pointer here is to storage that outlives the pumping of
|
||||
// ~58 KB page in PROGMEM. Every pointer here is to storage that outlives the pumping of
|
||||
// the response (literals and kIndexHtml live forever; the JSON lives in this struct,
|
||||
// which the caller keeps until the response is queued), which is what makes "build once,
|
||||
// queue on either transport" safe.
|
||||
|
||||
+208
-176
@@ -54,6 +54,19 @@
|
||||
* those positions are what travel as HID usage ids. If the TARGET is set to a
|
||||
* non-US layout, positions map to different characters -- the same caveat every
|
||||
* KVM has. Fix it on the target (set its layout to US) or remap CODE_TO_USAGE.
|
||||
*
|
||||
* Screen layout: this page is used on a laptop AND on a phone held in one hand
|
||||
* over a target you are standing at. One breakpoint (720 px) switches to a
|
||||
* single column with a sticky ARM bar; a coarse-pointer query puts every press
|
||||
* target on a 44 px thumb floor; inputs never render below 16 px because iOS
|
||||
* Safari zooms the page on focusing a smaller field and never zooms back out.
|
||||
* The ARM bar is sticky because armed-or-disarmed is the one fact that must
|
||||
* survive scrolling -- a state you have to scroll up to read is a state you
|
||||
* press blind. Pinch zoom stays on: the viewport only adds viewport-fit=cover,
|
||||
* and without it env(safe-area-inset-*) is always 0. Every one of these layout
|
||||
* claims is asserted by tests/test_page.js against the served bytes, because
|
||||
* no compiler reads this string and a missing breakpoint fails silently -- it
|
||||
* just renders the laptop layout onto a phone.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
@@ -69,7 +82,7 @@ static const char kIndexHtml[] PROGMEM =
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
||||
<title>keyrelay</title>
|
||||
<style>
|
||||
:root{
|
||||
@@ -89,10 +102,11 @@ h1 b{color:var(--accent)}
|
||||
.pill.on{color:var(--ok);border-color:#1f3b26;background:#12261a}
|
||||
.pill.off{color:var(--dim)}
|
||||
.pill.bad{color:var(--hot);border-color:#4a1d1d;background:#2a1214}
|
||||
main{padding:14px;display:grid;gap:14px;max-width:980px}
|
||||
main{padding:14px;padding-bottom:calc(14px + env(safe-area-inset-bottom));
|
||||
display:grid;gap:14px;max-width:980px}
|
||||
.arm{width:100%;padding:26px;font:inherit;font-size:22px;font-weight:700;letter-spacing:2px;
|
||||
color:var(--fg);background:var(--panel);border:2px solid var(--line);border-radius:12px;
|
||||
cursor:pointer}
|
||||
cursor:pointer;position:sticky;top:0;z-index:5;touch-action:manipulation;user-select:none}
|
||||
.arm:hover{border-color:var(--accent)}
|
||||
.arm.armed{color:#fff;background:#4a1216;border-color:var(--hot);
|
||||
box-shadow:0 0 0 1px var(--hot),0 0 26px -6px var(--hot)}
|
||||
@@ -109,13 +123,15 @@ main{padding:14px;display:grid;gap:14px;max-width:980px}
|
||||
.key{border:1px solid var(--line);border-radius:5px;padding:1px 7px;font-size:12px;background:#0b0f14}
|
||||
.key.mod{color:var(--accent);border-color:#1f3552}
|
||||
button.k{font:inherit;font-size:12.5px;color:var(--fg);background:#0b0f14;
|
||||
border:1px solid var(--line);border-radius:7px;padding:7px 9px;cursor:pointer}
|
||||
border:1px solid var(--line);border-radius:7px;padding:7px 9px;cursor:pointer;
|
||||
touch-action:manipulation;user-select:none}
|
||||
button.k:hover{border-color:var(--accent);color:#fff}
|
||||
button.k.danger{color:var(--warn);border-color:#4a3a12}
|
||||
button.k.danger:hover{border-color:var(--hot);color:#fff;background:#2a1214}
|
||||
button.k.armed-confirm{color:#fff;background:#4a1216;border-color:var(--hot)}
|
||||
.row{display:flex;flex-wrap:wrap;gap:6px;align-items:center}
|
||||
label.opt{display:flex;gap:7px;align-items:center;font-size:12.5px;color:var(--dim);cursor:pointer}
|
||||
label.opt{display:flex;gap:7px;align-items:center;font-size:12.5px;color:var(--dim);
|
||||
cursor:pointer;touch-action:manipulation}
|
||||
input[type=password],input[type=text]{font:inherit;font-size:13px;background:#0b0f14;
|
||||
color:var(--fg);border:1px solid var(--line);border-radius:7px;padding:6px 8px;width:150px}
|
||||
textarea{font:inherit;font-size:13px;background:#0b0f14;color:var(--fg);width:100%;
|
||||
@@ -128,7 +144,19 @@ button.k:disabled:hover{color:#4d565f;border-color:var(--line);background:#0b0f1
|
||||
.rowhead{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin-bottom:8px}
|
||||
.rowhead h2{margin:0}
|
||||
p.hint{color:var(--dim);font-size:11.5px;margin:8px 0 0}
|
||||
#log{height:150px;overflow-y:auto;font-size:12px;border:1px solid var(--line);border-radius:8px;
|
||||
.mt{margin-top:7px}
|
||||
.hot{color:var(--hot)}
|
||||
.dimv{color:var(--dim);font-weight:400}
|
||||
.dim{color:var(--dim)}
|
||||
/* The long explanations live behind a tap. A details element is zero script,
|
||||
* keyboard accessible, printable, works with JS off, and degrades to visible
|
||||
* text in a browser that ignores it -- the safe direction for a safety note. */
|
||||
details{margin:7px 0 0}
|
||||
summary{color:var(--accent);font-size:11.5px;cursor:pointer;
|
||||
touch-action:manipulation;user-select:none}
|
||||
details p{margin:6px 0 0}
|
||||
#log{height:150px;overflow-y:auto;overscroll-behavior:contain;font-size:12px;
|
||||
border:1px solid var(--line);border-radius:8px;
|
||||
background:#0b0f14;padding:7px 9px;white-space:pre-wrap}
|
||||
#log .e{color:var(--hot)}
|
||||
#log .w{color:var(--warn)}
|
||||
@@ -138,17 +166,45 @@ kbd{border:1px solid var(--line);border-bottom-width:2px;border-radius:4px;paddi
|
||||
font-size:11.5px;background:#0b0f14}
|
||||
.mousepad{height:118px;border:1px dashed var(--line);border-radius:10px;background:#0b0f14;
|
||||
display:flex;align-items:center;justify-content:center;text-align:center;padding:0 14px;
|
||||
color:var(--dim);font-size:12.5px;cursor:crosshair;user-select:none}
|
||||
color:var(--dim);font-size:12.5px;cursor:crosshair;user-select:none;touch-action:manipulation}
|
||||
.mousepad:hover{border-color:var(--accent);color:var(--fg)}
|
||||
.mousepad.captured{border-style:solid;border-color:var(--hot);color:var(--hot);
|
||||
background:#1d0d10;cursor:none}
|
||||
body.armed{outline:3px solid var(--hot);outline-offset:-3px}
|
||||
body.off{opacity:.92}
|
||||
/* One breakpoint (720 px) plus a pointer query: one the README can describe
|
||||
* beats five nobody can remember. main goes flex-column here because a sticky
|
||||
* *grid* item is pinned inside its own area and would never move. The 16 px
|
||||
* input floor is iOS Safari's focus-zoom threshold, not taste. */
|
||||
@media (max-width:720px){
|
||||
main{display:flex;flex-direction:column;gap:12px;padding:10px;
|
||||
padding-bottom:calc(10px + env(safe-area-inset-bottom))}
|
||||
.grid{grid-template-columns:1fr}
|
||||
header{padding:8px 10px}
|
||||
.arm{padding:18px;font-size:20px}
|
||||
input[type=password],input[type=text],textarea{font-size:16px}
|
||||
#log{height:auto;max-height:40vh}
|
||||
/* The counters are the "is it typing" evidence, so they stay visible -- they just
|
||||
* stop stacking into eight rows and pushing the ARM bar off a small screen. */
|
||||
.kv{display:flex;flex-wrap:wrap;gap:1px 10px}
|
||||
.kv div:nth-child(even){text-align:left}
|
||||
.rowhead{gap:8px;margin-bottom:6px}
|
||||
.rowhead label.opt{font-size:12px}
|
||||
}
|
||||
/* The thumb floor: on a coarse pointer every press target is 44 px tall, and
|
||||
* wide where it was narrow. Fine pointers keep the dense laptop layout. summary
|
||||
* is a press target too -- the folded notes are the only route to five safety
|
||||
* sentences, and it measured 17 px before it was on this list. */
|
||||
@media (pointer:coarse){
|
||||
button.k,.pill,input,summary{min-height:44px}
|
||||
button.k,input[type=checkbox]{min-width:44px}
|
||||
summary{padding:13px 0}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="off">
|
||||
<header>
|
||||
<h1>key<b>relay</b> <span id="ver" style="color:var(--dim);font-weight:400"></span></h1>
|
||||
<h1>key<b>relay</b> <span id="ver" class="dimv"></span></h1>
|
||||
<div class="pills">
|
||||
<span class="pill" id="pNet">network</span>
|
||||
<span class="pill" id="pUsb">target</span>
|
||||
@@ -173,11 +229,11 @@ body.off{opacity:.92}
|
||||
<div class="card">
|
||||
<h2>Special keys</h2>
|
||||
<div class="row">
|
||||
<button class="k danger" id="cad" title="Press twice within 3s. The laptop's own OS swallows the real Ctrl+Alt+Del, so it has to come from here. On a Linux virtual terminal this reboots the target.">
|
||||
<button class="k danger" id="cad" title="Press twice within 3s. The laptop's own OS eats the real one. On a Linux VT this reboots the target.">
|
||||
Ctrl+Alt+Del</button>
|
||||
<button class="k" id="release" title="All keys up, stay armed. Also Ctrl+Alt+Esc.">Release keys</button>
|
||||
</div>
|
||||
<div class="row" style="margin-top:6px">
|
||||
<div class="row mt">
|
||||
<button class="k" data-chord="05:3a" title="Ctrl+Alt+F1">⌐ VT1</button>
|
||||
<button class="k" data-chord="05:3b" title="Ctrl+Alt+F2">⌐ VT2</button>
|
||||
<button class="k" data-chord="05:3c" title="Ctrl+Alt+F3">⌐ VT3</button>
|
||||
@@ -187,41 +243,40 @@ body.off{opacity:.92}
|
||||
<button class="k" data-chord="00:46" title="PrintScreen">PrtSc</button>
|
||||
<button class="k" data-chord="00:65" title="Application menu key">Menu</button>
|
||||
</div>
|
||||
<div class="row" style="margin-top:6px">
|
||||
<button class="k" data-chord="08:50" title="Super+Left — KDE: snap the focused window to the left half of the screen">⇦ snap left</button>
|
||||
<button class="k" data-chord="08:4f" title="Super+Right — KDE: snap the focused window to the right half of the screen">snap right ⇨</button>
|
||||
<button class="k" data-chord="08:4b" title="Super+PageUp — KDE: make the focused window fullscreen">⛶ fullscreen</button>
|
||||
<div class="row mt">
|
||||
<button class="k" data-chord="08:50" title="Super+Left (KDE: window to the left half)">⇦ snap left</button>
|
||||
<button class="k" data-chord="08:4f" title="Super+Right (KDE: window to the right half)">snap right ⇨</button>
|
||||
<button class="k" data-chord="08:4b" title="Super+PageUp (KDE: fullscreen)">⛶ fullscreen</button>
|
||||
</div>
|
||||
<p style="color:var(--dim);font-size:11.5px;margin:8px 0 0">
|
||||
These are here because the laptop's window manager grabs them before the browser
|
||||
ever sees them — press Super+Left on the laptop and the laptop tiles its own
|
||||
window. So the bridge types the chord instead and the machine it is plugged into
|
||||
is the one that reacts. Which window does what is that machine's own keybinding
|
||||
(these three are KDE Plasma's: window left, window right, fullscreen). They need
|
||||
the bridge armed, and each one is a single deliberate click.
|
||||
<p class="hint">The laptop's window manager grabs these chords before the browser
|
||||
sees them, so the bridge types them instead — armed, one deliberate click each.
|
||||
</p>
|
||||
<details><summary>why these are buttons</summary><p class="hint">Press Super+Left on
|
||||
the laptop and the laptop tiles its own window — the browser never sees that
|
||||
chord. What a chord does on the target is that machine's own keybinding; these three
|
||||
are KDE Plasma's: window left, window right, fullscreen.</p></details>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>State</h2>
|
||||
<div class="kv">
|
||||
<div>mode</div><div id="sMode">—</div>
|
||||
<div>address</div><div id="sIp">—</div>
|
||||
<div>uptime</div><div id="sUp">—</div>
|
||||
<div>keys sent</div><div id="sKeys">0</div>
|
||||
<div>text sent</div><div id="sTyped">0</div>
|
||||
<div>ip</div><div id="sIp">—</div>
|
||||
<div>up</div><div id="sUp">—</div>
|
||||
<div>keys</div><div id="sKeys">0</div>
|
||||
<div>text</div><div id="sTyped">0</div>
|
||||
<div>chords</div><div id="sChords">0</div>
|
||||
<div>mouse reports</div><div id="sMouse">0</div>
|
||||
<div>mouse</div><div id="sMouse">0</div>
|
||||
<div>dropped</div><div id="sDrop">0</div>
|
||||
</div>
|
||||
<h2 style="margin-top:10px">Held now</h2>
|
||||
<div class="keys" id="held"><span style="color:var(--dim)">nothing</span></div>
|
||||
<h2 class="mt">Held now</h2>
|
||||
<div class="keys" id="held"><span class="dim">nothing</span></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Connection</h2>
|
||||
<div class="banner" id="clearWarn" hidden>
|
||||
<b style="color:var(--hot)">This page arrived in the clear.</b> A TLS board
|
||||
<b class="hot">This page arrived in the clear.</b> A TLS board
|
||||
answers plaintext requests with a redirect and never with these bytes, so an
|
||||
<code>http://</code> copy of this page is a stale cache, a downgrade, or
|
||||
somebody else’s server — and it will not take a passphrase. Reload
|
||||
@@ -231,20 +286,19 @@ body.off{opacity:.92}
|
||||
<div class="row">
|
||||
<input type="password" id="token" placeholder="passphrase" autocomplete="off">
|
||||
<button class="k" id="connect">connect</button>
|
||||
<button class="k danger" id="takeover" hidden title="Appears only when the board says another controller holds the bridge. One deliberate click: authenticate, then ask for the bridge (X). The other session is told (E12), every key and every mouse button is released, and the board comes back DISARMED — you arm again on purpose.">take over</button>
|
||||
<button class="k danger" id="takeover" hidden title="Only shown when the board says another controller holds the bridge. One click takes it; the board lands DISARMED.">take over</button>
|
||||
</div>
|
||||
<label class="opt" style="margin-top:9px">
|
||||
<label class="opt mt">
|
||||
<input type="checkbox" id="keepArmed">
|
||||
stay armed if this tab loses focus
|
||||
</label>
|
||||
<p style="color:var(--dim);font-size:11.5px;margin:8px 0 0">
|
||||
The passphrase is checked by the board on every connection. Without it nothing
|
||||
can be armed, so anyone else on the network gets a page that does nothing.
|
||||
<p class="hint">The board checks this on every connection; without it nothing can
|
||||
arm, so anyone else on the network gets a page that does nothing.</p>
|
||||
<details><summary>what the passphrase does and does not buy</summary><p class="hint">
|
||||
Someone who <b>has</b> it can take the bridge from a session left connected
|
||||
— that is what <b>take over</b> is for — and cannot arm it by doing
|
||||
so: every handover lands disarmed, with the other session’s keys and
|
||||
buttons released and its screen told.
|
||||
</p>
|
||||
so: every handover lands disarmed, every key and button released, the other screen
|
||||
told.</p></details>
|
||||
<p class="hint" id="certFp" hidden></p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -252,35 +306,34 @@ body.off{opacity:.92}
|
||||
<div class="card">
|
||||
<div class="rowhead">
|
||||
<h2>Mouse</h2>
|
||||
<button class="k" id="mCapture" title="Asks the browser for Pointer Lock, the same thing a web game does: your cursor cannot escape this tab while the board is forwarding it. Escape gives it back.">capture the pointer</button>
|
||||
<button class="k" id="mCapture" title="Pointer Lock, as a web game uses it: the cursor cannot leave this tab while forwarded. Esc gives it back.">capture the pointer</button>
|
||||
<button class="k danger" id="mRelease" title="Every mouse button up. The cursor stays captured.">release buttons</button>
|
||||
<span class="pill off" id="mStat">not captured</span>
|
||||
</div>
|
||||
<div class="mousepad" id="mPad" tabindex="-1">
|
||||
<span id="mPadText">click here to hand this tab the mouse</span>
|
||||
</div>
|
||||
<div class="row" style="margin-top:8px">
|
||||
<span style="color:var(--dim);font-size:12.5px">buttons held</span>
|
||||
<div class="keys" id="mHeld"><span style="color:var(--dim)">none</span></div>
|
||||
<div class="row mt">
|
||||
<span class="dim">buttons held</span>
|
||||
<div class="keys" id="mHeld"><span class="dim">none</span></div>
|
||||
<span class="pill off" id="mFrames">0 reports</span>
|
||||
</div>
|
||||
<p class="hint">
|
||||
Movement is <b>relative</b>, like a real mouse: the board says how far the pointer
|
||||
moved, never where it is, so the target's own pointer speed and acceleration decide
|
||||
where that lands. Buttons are restated on every frame, which is why a frame lost in
|
||||
the WiFi cannot leave one held down. <kbd>Esc</kbd> frees the cursor and lets go of
|
||||
every button — the browser keeps that key for itself, so it never reaches the
|
||||
<p class="hint">Movement is <b>relative</b>, like a real mouse. <kbd>Esc</kbd> frees
|
||||
the cursor and lets go of every button.</p>
|
||||
<details><summary>how a forwarded mouse behaves</summary><p class="hint">The board says
|
||||
how far the pointer moved, never where it is: the target's own pointer speed decides where
|
||||
it lands. Buttons are restated on every frame, which is why a frame lost in the WiFi
|
||||
cannot leave one held down. The browser keeps <kbd>Esc</kbd>, so it never reaches the
|
||||
target, and losing the tab does the same. No auto-click, no macro, no saved gesture,
|
||||
nothing scheduled: if nobody is moving a mouse, nothing moves. Capture needs the
|
||||
bridge armed and this board's mouse interface on (<code>mouse on</code> over serial).
|
||||
</p>
|
||||
nothing scheduled. Capture needs the bridge armed and <code>mouse on</code> over
|
||||
serial.</p></details>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Send text</h2>
|
||||
<textarea id="paste" rows="3" spellcheck="false" autocomplete="off"
|
||||
placeholder="paste text here. Nothing is sent until you press the button below." ></textarea>
|
||||
<div class="row" style="margin-top:7px">
|
||||
<div class="row mt">
|
||||
<button class="k" id="typeSend" disabled>type 0 characters</button>
|
||||
<button class="k danger" id="typeStop" disabled>stop typing</button>
|
||||
<label class="opt" title="Off by default: a paste that quietly loses a character is worse than one that refuses">
|
||||
@@ -289,14 +342,13 @@ body.off{opacity:.92}
|
||||
<input type="checkbox" id="trailingEnter">press Enter when done</label>
|
||||
<span class="pill off" id="typeStat">idle</span>
|
||||
</div>
|
||||
<p class="hint" id="typeHint">
|
||||
The board types these characters as keystrokes, at a pace it keeps itself, so the
|
||||
target sees a keyboard and not a paste. It needs the bridge armed, and one press of
|
||||
this button — there is no history, nothing is saved, and nothing is ever typed
|
||||
later. <b>stop typing</b> or <kbd>Ctrl+Alt+Esc</kbd> abandons the rest of a run.
|
||||
Layout matters: characters are positions, so the target must be set to a US layout
|
||||
for these to land as they read here.
|
||||
</p>
|
||||
<p class="hint" id="typeHint">The board types these as keystrokes, while armed.
|
||||
<b>stop typing</b> or <kbd>Ctrl+Alt+Esc</kbd> abandons the rest of a run. Characters are
|
||||
positions, so the target must be set to a US layout.</p>
|
||||
<details><summary>what a run is and is not</summary><p class="hint">The pace is the
|
||||
board's own, so the target sees a keyboard, not a paste, and that button is the only
|
||||
trigger: no history, nothing is saved, nothing typed later. Disarming, losing this tab
|
||||
or a board error ends a run too.</p></details>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
@@ -311,11 +363,11 @@ body.off{opacity:.92}
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
Shortcuts while armed: <kbd>Ctrl+Alt+Esc</kbd> release every key and every mouse button
|
||||
and disarm · <kbd>Ctrl+Alt+d</kbd> the Ctrl+Alt+Del confirm · while the
|
||||
pointer is captured <kbd>Esc</kbd> belongs to the browser and frees the cursor ·
|
||||
every other key is forwarded to the target untouched.
|
||||
Nothing is forwarded while disarmed; reload, focus loss and socket loss all disarm.
|
||||
<details><summary>shortcuts while armed</summary><p class="hint">
|
||||
<kbd>Ctrl+Alt+Esc</kbd> releases every key and every mouse button and disarms ·
|
||||
<kbd>Ctrl+Alt+d</kbd> is the Ctrl+Alt+Del confirm · while captured, <kbd>Esc</kbd>
|
||||
belongs to the browser · every other key goes to the target untouched.</p></details>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
@@ -337,11 +389,11 @@ body.off{opacity:.92}
|
||||
"var KR_TLS_BUILD=0;"
|
||||
#endif
|
||||
R"HTML(
|
||||
/* ------------------------------------------------------------------ *
|
||||
/* ---------------------------------------- *
|
||||
* KeyboardEvent.code -> USB HID usage id (usage page 0x07).
|
||||
* code names a physical position, so this table is US-ANSI based; it is
|
||||
* the one thing to edit for a different board.
|
||||
* ------------------------------------------------------------------ */
|
||||
* ---------------------------------------- */
|
||||
const CODE_TO_USAGE = {
|
||||
KeyA:0x04,KeyB:0x05,KeyC:0x06,KeyD:0x07,KeyE:0x08,KeyF:0x09,KeyG:0x0a,KeyH:0x0b,
|
||||
KeyI:0x0c,KeyJ:0x0d,KeyK:0x0e,KeyL:0x0f,KeyM:0x10,KeyN:0x11,KeyO:0x12,KeyP:0x13,
|
||||
@@ -381,13 +433,11 @@ const LOG_LINES = 150;
|
||||
/**
|
||||
* One line per thing worth reading.
|
||||
*
|
||||
* Two rules, both of them here rather than at the call sites, because a log you
|
||||
* cannot read is worse than no log:
|
||||
*
|
||||
* - nothing empty ever lands. A blank line is pure height, and a few of them a
|
||||
* second pushed the actual key lines off the visible area for good.
|
||||
* - an identical line repeating is counted, not repeated. Three hundred copies of
|
||||
* the same retry say one thing and say it once.
|
||||
* Two rules, here rather than at the call sites, because a log you cannot read is
|
||||
* worse than no log: nothing empty ever lands (a blank line is pure height, and a
|
||||
* few a second pushed the key lines off the visible area); and an identical line
|
||||
* repeating is counted, not stacked -- three hundred copies of one retry say one
|
||||
* thing, once.
|
||||
*/
|
||||
function log(msg, cls) {
|
||||
const body = (msg === undefined || msg === null) ? "" : String(msg);
|
||||
@@ -422,10 +472,10 @@ function logKey(label, frame) {
|
||||
log("key " + label + " " + frame, "g");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
/* ---------------------------------------- *
|
||||
* Local key state. We restate the WHOLE state on every event, so a lost
|
||||
* frame cannot leave a key stuck: the next restatement fixes everything.
|
||||
* ------------------------------------------------------------------ */
|
||||
* ---------------------------------------- */
|
||||
let mods = 0;
|
||||
let keys = []; // usage ids, press order
|
||||
|
||||
@@ -442,7 +492,7 @@ function paintHeld() {
|
||||
const bits = [];
|
||||
for (let i = 0; i < 8; i++) if (mods & (1 << i)) bits.push(MOD_NAME[i]);
|
||||
for (const u of keys) bits.push(USAGE_NAME[u] || ("0x" + u.toString(16)));
|
||||
if (!bits.length) { el.innerHTML = '<span style="color:var(--dim)">nothing</span>'; return; }
|
||||
if (!bits.length) { el.innerHTML = '<span class="dim">nothing</span>'; return; }
|
||||
for (const b of bits) {
|
||||
const s = document.createElement("span");
|
||||
s.className = "key" + (MOD_NAME.includes(b) ? " mod" : "");
|
||||
@@ -474,29 +524,25 @@ function describePress(code) {
|
||||
return (held.length ? held.join("+") + "+" : "") + self;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
/* ---------------------------------------- *
|
||||
* Link
|
||||
* ------------------------------------------------------------------ */
|
||||
* ---------------------------------------- */
|
||||
let ws = null, armed = false, authed = false, reconnectAt = 0, status = null;
|
||||
|
||||
/* Facts about the link that are not the socket object itself.
|
||||
* gotOk / gotStatus what THIS socket has been told: a controller gets `OK`
|
||||
* and a status frame immediately; a pending claim (phase 03,
|
||||
* the board held by someone else) gets `OK` and no status --
|
||||
* the status frame belongs to the controller alone. "OK then
|
||||
* never adopted" is therefore the board reporting `busy` in
|
||||
* the only terms it gives a socket that does not hold the
|
||||
* bridge.
|
||||
* busyKnown the board has said a controller holds the bridge: by not
|
||||
* adopting us, by E12, or by a status frame saying busy while
|
||||
* we are not the controller.
|
||||
* takenOver our own session was displaced by another controller (E12).
|
||||
* autoReconnect whether the 2 s loop may reconnect at all. Turned off by
|
||||
* exactly those two facts: a page that immediately re-claims
|
||||
* turns two tabs into a fight over a keyboard. It comes back
|
||||
* on the next deliberate click (connect, or take over).
|
||||
* pendingTakeover this socket was opened by the take-over button; its first
|
||||
* status frame is the moment to say what happened. */
|
||||
* gotOk / gotStatus what THIS socket has been told. A controller gets `OK` and a
|
||||
* status frame at once; a pending claim (phase 03) gets `OK` and
|
||||
* no status, because that frame belongs to the controller alone.
|
||||
* So "OK, then never adopted" IS the board reporting `busy` in
|
||||
* the only terms it gives a socket that does not hold the bridge.
|
||||
* busyKnown the board said a controller holds it: by not adopting us, by
|
||||
* E12, or by a busy frame while we are not the controller.
|
||||
* takenOver our own session was displaced (E12).
|
||||
* autoReconnect whether the 2 s loop may reconnect -- off for exactly those two
|
||||
* facts, because re-claiming at once turns two tabs into a fight
|
||||
* over a keyboard. Back on the next deliberate click.
|
||||
* pendingTakeover this socket came from the take-over button; its first status
|
||||
* frame is the moment to say what happened. */
|
||||
let gotOk = false, gotStatus = false, busyKnown = false, takenOver = false;
|
||||
let autoReconnect = true, pendingTakeover = false;
|
||||
|
||||
@@ -508,13 +554,11 @@ function setPill(cls, text) {
|
||||
|
||||
function showTakeover(on) { $("takeover").hidden = !on; }
|
||||
|
||||
/* The TLS build's page on an http: origin is a page nobody legitimate served: a
|
||||
* KR_TLS=1 board answers :80 with a redirect and never with these bytes, so an
|
||||
* http: tab is a cache, a downgrade, or somebody else's server. Refuse before the
|
||||
* passphrase can leave it. This is the guard the other way round from the scheme
|
||||
* choice below -- and it never falls back to the plaintext scheme, because
|
||||
* silently taking the weaker option is the failure this transport exists to
|
||||
* prevent. */
|
||||
/* A TLS build's page on an http: origin is bytes nobody legitimate served: a
|
||||
* KR_TLS=1 board answers :80 with a redirect, never with these. So an http: tab is
|
||||
* a cache, a downgrade, or somebody else's server -- refuse before the passphrase
|
||||
* can leave it. It never falls back to the plaintext scheme: silently taking the
|
||||
* weaker option is the failure this transport exists to prevent. */
|
||||
function servedInClear() {
|
||||
return KR_TLS_BUILD === 1 && location.protocol !== "https:";
|
||||
}
|
||||
@@ -528,16 +572,14 @@ function tokenValue() {
|
||||
function connect() { openSession(false); }
|
||||
|
||||
/**
|
||||
* One socket builder for both gestures: ordinary connect, and the deliberate
|
||||
* take-over. `takeover` means: after the passphrase send `X` and nothing else
|
||||
* -- no H, no state, no keepalive -- because one frame is one command and one
|
||||
* click is one intent. Before the click nothing about the bridge is sent; after
|
||||
* it the page is an ordinary controller, starting DISARMED, because the handover
|
||||
* itself lands disarmed (control_server.cpp handOverClaim) and only an `A1` typed
|
||||
* from here on arms. `H` on an ordinary socket doubles as the probe that says
|
||||
* which one we are: a controller answers it with a status frame, the board's one
|
||||
* pending-claim slot answers it with E6 and a drop -- and the drop is what
|
||||
* onGone() reads as "held by another controller".
|
||||
* One socket builder for both gestures: connect, and the deliberate take-over.
|
||||
* `takeover` means: after the passphrase send `X` and nothing else -- no H, no
|
||||
* state, no keepalive -- one frame one command, one click one intent. Before the
|
||||
* click nothing about the bridge is sent; after it this is an ordinary controller,
|
||||
* starting DISARMED, because the handover lands disarmed (control_server.cpp
|
||||
* handOverClaim) and only an `A1` from here on arms. `H` is also the probe that
|
||||
* says which one we are: a controller answers with a status frame, the pending-claim
|
||||
* slot with E6 and a drop -- and onGone() reads that drop as "held".
|
||||
*/
|
||||
function openSession(takeover) {
|
||||
if (servedInClear()) {
|
||||
@@ -568,10 +610,10 @@ function openSession(takeover) {
|
||||
}
|
||||
|
||||
/* The socket went -- and which sentence is true depends on what the board said
|
||||
* before it did. This is where a refused or un-adopted upgrade stops being a
|
||||
* 2-second retry loop: a page that keeps re-claiming against a controller that
|
||||
* is not coming back just occupies the board's one claim slot, so the page comes
|
||||
* down and waits for one deliberate click instead. */
|
||||
* before it did. This is where a refused or un-adopted upgrade stops being a 2 s
|
||||
* retry loop: re-claiming against a controller that is not coming back just
|
||||
* occupies the board's one claim slot, so the page comes down and waits for one
|
||||
* deliberate click. */
|
||||
function onGone() {
|
||||
authed = false;
|
||||
if (takenOver) {
|
||||
@@ -584,10 +626,10 @@ function onGone() {
|
||||
}
|
||||
setArmed(false, "link lost");
|
||||
if (gotOk && !gotStatus) {
|
||||
/* Authenticated, never adopted: the board put us in its spare slot as a
|
||||
* pending claim, which IS the board reporting busy -- a controller holds the
|
||||
* keyboard and the status frame that would have said so belongs to it alone.
|
||||
* Counter-and-link-state terms only: we are told that it exists, never who. */
|
||||
/* Authenticated, never adopted: the board put us in its spare slot as a pending
|
||||
* claim, which IS the board reporting busy -- a controller holds the keyboard and
|
||||
* the status frame saying so belongs to it alone. Link state only: that it exists,
|
||||
* never who. */
|
||||
busyKnown = true;
|
||||
setPill("bad", "held by another controller");
|
||||
showTakeover(true);
|
||||
@@ -646,13 +688,12 @@ function onFrame(raw) {
|
||||
setArmed(false, "no room for a claim: another controller holds the bridge");
|
||||
}
|
||||
else if (code === "12") {
|
||||
/* E12 is "you were DROPPED": another controller took this bridge, and it
|
||||
* arrived through the release funnel -- the board has already released every
|
||||
* key and every mouse button and is DISARMED. The local mirror is the same
|
||||
* gesture: setArmed(false) ends the typing run, the keys and the mouse here,
|
||||
* and the log carries the reason the operator of this screen needs to read.
|
||||
* Then this page stays down; auto-reclaiming is how two tabs become a fight,
|
||||
* and coming back is one deliberate click on TAKE OVER. */
|
||||
/* E12 is "you were DROPPED": another controller took this bridge through the
|
||||
* release funnel -- every key and every mouse button already released, the board
|
||||
* DISARMED. The local mirror is the same gesture: setArmed(false) ends the typing
|
||||
* run, the keys and the mouse here, and the log says why for whoever is at this
|
||||
* screen. Then this page stays down -- auto-reclaiming is how two tabs become a
|
||||
* fight, and coming back is one deliberate click on TAKE OVER. */
|
||||
takenOver = true; busyKnown = true; autoReconnect = false;
|
||||
setArmed(false, "another controller took over");
|
||||
setPill("bad", "taken over");
|
||||
@@ -667,10 +708,10 @@ function onFrame(raw) {
|
||||
log("board: " + raw);
|
||||
}
|
||||
|
||||
/* The channel and its certificate, rendered from the status frame only. `tls` is
|
||||
* what the board compiled in and holds; `fp` is the label of the certificate it
|
||||
* actually serves -- never assembled from anything this page invents, because the
|
||||
* operator compares these exact bytes against `cert` on the serial console. */
|
||||
/* The channel and its certificate, rendered from the status frame only: `tls` is
|
||||
* what the board holds, `fp` the label of the certificate it actually serves --
|
||||
* never assembled from anything this page invents, because the operator compares
|
||||
* these exact bytes against `cert` on the serial console. */
|
||||
function paintChannel(s) {
|
||||
const p = $("pTls");
|
||||
if (s.tls) { p.className = "pill on"; p.textContent = "tls"; }
|
||||
@@ -691,10 +732,10 @@ function applyStatus(s) {
|
||||
status = s;
|
||||
paintChannel(s);
|
||||
if (pendingTakeover) {
|
||||
/* The frame the handover promised: this page asked for the bridge with one
|
||||
* click, the board ran the funnel, and what it says now is the truth about
|
||||
* the state we start in -- DISARMED. Saying it out loud is the difference
|
||||
* between "it works again" and "it works again, from zero". */
|
||||
/* The frame the handover promised: this page asked for the bridge with one click,
|
||||
* the board ran the funnel, and what it says now is the truth about the state we
|
||||
* start in -- DISARMED. Saying it out loud is the difference between "it works
|
||||
* again" and "it works again, from zero". */
|
||||
pendingTakeover = false;
|
||||
log("bridge taken over -- the board is DISARMED; arm again on purpose", "g");
|
||||
}
|
||||
@@ -731,7 +772,7 @@ function applyStatus(s) {
|
||||
if (mButtons !== (s.mbtn || 0) && !mCaptured) paintMouse();
|
||||
refreshMouseUi();
|
||||
if (s.dflt) {
|
||||
$("banner").innerHTML = '<b style="color:var(--hot)">The board is still using the '
|
||||
$("banner").innerHTML = '<b class="hot">The board is still using the '
|
||||
+ 'default passphrase.</b> Set your own with the serial command <code>token '
|
||||
+ '<passphrase></code> before you plug this into anything.';
|
||||
}
|
||||
@@ -740,10 +781,10 @@ function applyStatus(s) {
|
||||
refreshSendBox();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
/* ---------------------------------------- *
|
||||
* Arming. Off is the only safe default, so this is the one state change
|
||||
* that always travels, and that the board also controls (its button).
|
||||
* ------------------------------------------------------------------ */
|
||||
* ---------------------------------------- */
|
||||
function setArmed(on, why) {
|
||||
armed = on;
|
||||
const b = $("arm");
|
||||
@@ -770,9 +811,9 @@ $("arm").addEventListener("click", () => {
|
||||
/* The board's status heartbeat confirms it; no optimistic flip. */
|
||||
});
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
/* ---------------------------------------- *
|
||||
* Key capture
|
||||
* ------------------------------------------------------------------ */
|
||||
* ---------------------------------------- */
|
||||
document.addEventListener("keydown", ev => {
|
||||
/* Ctrl+Alt+Esc is the panic key, armed or not: it must always work. */
|
||||
if (ev.ctrlKey && ev.altKey && ev.code === "Escape") {
|
||||
@@ -859,9 +900,9 @@ document.addEventListener("visibilitychange", () => {
|
||||
if (document.hidden) dropEverything("tab hidden");
|
||||
});
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
/* ---------------------------------------- *
|
||||
* Special keys
|
||||
* ------------------------------------------------------------------ */
|
||||
* ---------------------------------------- */
|
||||
let cadArmedUntil = 0;
|
||||
function armCad() {
|
||||
const now = Date.now();
|
||||
@@ -903,32 +944,25 @@ function fire(frame) {
|
||||
log("chord " + frame, "g");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
/* ---------------------------------------- *
|
||||
* Mouse
|
||||
*
|
||||
* Capture, not grab. This tab asks the browser for Pointer Lock -- the
|
||||
* same request a web game makes -- and while it holds it, the cursor
|
||||
* cannot leave the tab and every movement goes to the board. Escape
|
||||
* always gives the cursor back: that is the browser's own guarantee and
|
||||
* nothing here can take it away, which is why the mouse does not need a
|
||||
* Capture, not grab: Pointer Lock, the request a web game makes. While this tab holds
|
||||
* it the cursor cannot leave the tab and every movement goes to the board; Escape
|
||||
* always gives it back, the browser's own guarantee -- which is why the mouse needs no
|
||||
* second panic key.
|
||||
*
|
||||
* What goes on the wire is one report per frame: the complete button
|
||||
* byte, then this frame's movement. Buttons are restated every frame like
|
||||
* the keys are, so a lost frame did not click and the next frame is the
|
||||
* truth either way. Movement is a delta with no accumulated position
|
||||
* anywhere, so a lost frame means the pointer travelled a little less --
|
||||
* which is all a relative mouse ever promises. That asymmetry is why
|
||||
* dropping the tail of a huge movement is reported rather than fatal, and
|
||||
* why a queue overflow on a mouse frame still releases and disarms: the
|
||||
* frame that does not fit may be the one letting go of a button.
|
||||
* One report per frame: the whole button byte, then this frame's movement. Buttons are
|
||||
* restated like the keys, so a lost frame did not click; movement is a delta with no
|
||||
* position accumulated anywhere, so a lost frame travels a little less -- all a relative
|
||||
* mouse promises. That asymmetry is why an over-long movement is reported rather than
|
||||
* fatal, and why a mouse-frame overflow still releases and disarms: the frame that does
|
||||
* not fit may be the one letting go of a button.
|
||||
*
|
||||
* The rate is capped in the browser, not on the board: MOUSE_FRAME_MS is
|
||||
* the shortest interval between frames out of this tab, and MOUSE_MAX_FRAMES
|
||||
* how many one flush may emit. Beyond that the movement is dropped and
|
||||
* said out loud -- a cursor that lagged is survivable, a silent divergence
|
||||
* is not.
|
||||
* ------------------------------------------------------------------ */
|
||||
* The rate is capped here, not on the board (MOUSE_FRAME_MS, MOUSE_MAX_FRAMES), and the
|
||||
* excess is said out loud: a lagging cursor is survivable, a silent divergence is not.
|
||||
* README "The mouse" is the durable version of all of this.
|
||||
* ---------------------------------------- */
|
||||
const MOUSE_DELTA_MAX = 127; // one report's worth == config.h kMouseDeltaMax
|
||||
const MOUSE_BUTTON_MASK = 0x1f; // five buttons == config.h kMouseButtonMask
|
||||
const MOUSE_FRAME_MS = 8; // 125 Hz: the ceiling this tab sends at
|
||||
@@ -960,7 +994,7 @@ function paintMouse() {
|
||||
held.textContent = "";
|
||||
const bits = [];
|
||||
for (const bit of [0x01, 0x02, 0x04, 0x08, 0x10]) if (mButtons & bit) bits.push(bit);
|
||||
if (!bits.length) held.innerHTML = '<span style="color:var(--dim)">none</span>';
|
||||
if (!bits.length) held.innerHTML = '<span class="dim">none</span>';
|
||||
for (const bit of bits) {
|
||||
const s = document.createElement("span");
|
||||
s.className = "key mod";
|
||||
@@ -1129,22 +1163,20 @@ document.addEventListener("wheel", ev => {
|
||||
* often than every MOUSE_FRAME_MS, however fast the browser delivers events. */
|
||||
setInterval(() => { if (mCaptured) flushMouse(); }, MOUSE_FRAME_MS);
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
/* ---------------------------------------- *
|
||||
* Pasted text
|
||||
*
|
||||
* A human pasted this into the box and pressed its button -- that is the whole
|
||||
* justification for it. The difference between a remote keyboard and an
|
||||
* unattended injection device is a deliberate action with the arming light on,
|
||||
* and this keeps both. It is not a macro facility: there is no history, nothing
|
||||
* is scheduled or repeated, and nothing outlives the run.
|
||||
* A human pasted this and pressed its button -- the whole justification. The
|
||||
* difference between a remote keyboard and an unattended injection device is a
|
||||
* deliberate action with the arming light on, and this keeps both: no history,
|
||||
* nothing scheduled or repeated, nothing outliving the run.
|
||||
*
|
||||
* The board does the typing and keeps the pace, so a stall in this tab cannot
|
||||
* become a burst of keys on the target. Flow control is the board's own `typing`
|
||||
* count -- the next piece goes out when the board says it is empty, not when a
|
||||
* timer guessed it would be. It holds kTextJobCap characters, far more than one
|
||||
* frame's worth, so a heartbeat that was already in flight when a frame was sent
|
||||
* can let one piece start early without ever overrunning the buffer.
|
||||
* ------------------------------------------------------------------ */
|
||||
* The board types and keeps the pace, so a stall in this tab cannot become a burst
|
||||
* of keys on the target. Flow control is the board's own `typing` count: the next
|
||||
* piece goes out when the board says it is empty, not when a timer guessed. It
|
||||
* holds kTextJobCap characters, far more than one frame's worth, so a heartbeat
|
||||
* already in flight can let one piece start early without overrunning the buffer.
|
||||
* ---------------------------------------- */
|
||||
const TEXT_FRAME_MAX = 120; // characters per P= frame == config.h kTextFrameMax
|
||||
const TEXT_JOB_MAX = 2048; // characters per run == config.h kTextJobCap
|
||||
const TEXT_PACE_MS = 12; // milliseconds per character we ask for
|
||||
|
||||
+4
-4
@@ -67,7 +67,7 @@ namespace {
|
||||
// tests/test_relayctl.py ("the page the transport sends"). The page carries one
|
||||
// 19-byte #if KR_TLS build marker and both spellings are the same length, so this
|
||||
// number is build-independent -- a fact test_relayctl pins, not a hope.
|
||||
constexpr size_t kServedPageBytes = 59286;
|
||||
constexpr size_t kServedPageBytes = 59262;
|
||||
|
||||
/** The pump's shape, without a socket: hand out chunks, accept what a peer takes. */
|
||||
struct DrainLog {
|
||||
@@ -311,10 +311,10 @@ int main() {
|
||||
CHECK_EQ(fs.sent(), 20);
|
||||
|
||||
CASE("flash send: the real page, on the real budget");
|
||||
// 59 286 bytes (the count .agents/validate.sh prints) through kPageChunkBytes per write
|
||||
// 59 262 bytes (the count .agents/validate.sh prints) through kPageChunkBytes per write
|
||||
// and kPageBytesPerTick per tick: the exact numbers the device check compares against.
|
||||
{
|
||||
// A stand-in of the served page's size (59 286 B, the `page bytes:` line the gate
|
||||
// A stand-in of the served page's size (59 262 B, the `page bytes:` line the gate
|
||||
// prints). tests/test_relayctl.py pins this constant against the raw string in
|
||||
// src/web_page.h, so if the page changes and this number does not, a check fails --
|
||||
// the same "test the table" rule as the mouse bits and the evdev codes.
|
||||
@@ -342,7 +342,7 @@ int main() {
|
||||
CHECK_EQ(total, sizeof(big));
|
||||
CHECK(worstPiece <= kPageChunkBytes);
|
||||
CHECK(worstTick <= kPageBytesPerTick);
|
||||
// 59 286 / 1460 = 41 chunks; / 5840 per tick = 11 ticks. If either number changes in
|
||||
// 59 262 / 1460 = 41 chunks; / 5840 per tick = 11 ticks. If either number changes in
|
||||
// config.h this line moves, and the report has to move with it.
|
||||
CHECK_EQ((sizeof(big) + kPageBytesPerTick - 1) / kPageBytesPerTick, 11);
|
||||
CHECK_EQ((sizeof(big) + kPageChunkBytes - 1) / kPageChunkBytes, 41);
|
||||
|
||||
+354
-3
@@ -9,8 +9,12 @@
|
||||
// that matter, which are what the log is allowed to contain, which characters may
|
||||
// leave the tab, and when a run of pasted text stops.
|
||||
//
|
||||
// It is a behaviour test, not a rendering test: no CSS, no layout, no real keyboard.
|
||||
// tests/run_tests.sh skips it when node is not installed.
|
||||
// It is not a rendering test -- nothing here lays out a page. But it is the only
|
||||
// place the served bytes can be seen without a browser, so the CSS a phone is the
|
||||
// only real judge of (breakpoint, sticky ARM bar, thumb floors) is asserted at the
|
||||
// byte level, the safety sentences are asserted as text read out of the markup,
|
||||
// and the script itself is executed. Behaviour plus those pins is what a raw
|
||||
// string gets. tests/run_tests.sh skips this file when node is not installed.
|
||||
"use strict";
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
@@ -183,7 +187,9 @@ if (from < 0 || to < 0) {
|
||||
const pageScript = served.slice(from + "<script>".length, to);
|
||||
|
||||
const context = vm.createContext(sandbox);
|
||||
vm.runInContext(pageScript + expose, context);
|
||||
// filename so a throw inside the page names web_page.js, and so a V8 coverage
|
||||
// run (NODE_V8_COVERAGE=… node tests/test_page.js) can pick the page script out.
|
||||
vm.runInContext(pageScript + expose, context, { filename: "web_page.js" });
|
||||
const x = sandbox.__x;
|
||||
const logEl = x.logEl;
|
||||
|
||||
@@ -739,6 +745,342 @@ checkEq(!!x.inPageField({ target: null }), false, "a missing target does not thr
|
||||
"coming back is one deliberate click away, never zero");
|
||||
x.setArmed(false, "harness: done");
|
||||
|
||||
console.log("arming and the S= state frames");
|
||||
// Invariants 1 and 2, measured as bytes on the wire: disarmed forwards nothing,
|
||||
// arming is a REQUEST the board answers (the page never flips its own state),
|
||||
// and every keystroke restates the WHOLE state -- S=<mods>:<usages> -- so the
|
||||
// next frame corrects any lost one. The safety section below pins what the page
|
||||
// says; this pins what it sends, which is the half a restructure of the markup
|
||||
// could silently have moved.
|
||||
const sframes = () => sentFrames.filter(f => f.startsWith("S="));
|
||||
const stateWire = []; // every S= frame emitted this section
|
||||
const key = (type, code, extra) => {
|
||||
const before = sentFrames.length;
|
||||
docListeners.fire(type, Object.assign(
|
||||
{ code, target: { tagName: "BODY" }, preventDefault() {} }, extra || {}));
|
||||
for (const f of sentFrames.slice(before)) if (f.startsWith("S=")) stateWire.push(f);
|
||||
};
|
||||
|
||||
sandbox.location.protocol = "https:";
|
||||
byId("token").value = "hunter2";
|
||||
x.authed = false; x.armed = false;
|
||||
FakeWebSocket.instances.length = 0;
|
||||
x.connect();
|
||||
await wait(5); // onopen: T + H go out, authed stays false
|
||||
sentFrames.length = 0;
|
||||
byId("arm").fire("click");
|
||||
checkEq(sentFrames.length, 0,
|
||||
"an ARM click before the board has authenticated this socket asks for nothing");
|
||||
const live = FakeWebSocket.instances[FakeWebSocket.instances.length - 1];
|
||||
live.onmessage({ data: "OK" });
|
||||
checkEq(x.authed, true, "the board's OK is what links the page");
|
||||
|
||||
sentFrames.length = 0;
|
||||
byId("arm").fire("click");
|
||||
checkEq(sentFrames.join(","), "A1",
|
||||
"the ARM click sends A1 and nothing else -- one frame, one command");
|
||||
checkEq(x.armed, false,
|
||||
"and the page does not arm itself on its own say-so -- no optimistic flip, " +
|
||||
"the status heartbeat confirms or it did not happen");
|
||||
x.onFrame('S{"armed":1,"usb":1,"typing":0}');
|
||||
checkEq(x.armed, true, "the board's status frame is what arms the page");
|
||||
|
||||
sentFrames.length = 0;
|
||||
byId("arm").fire("click"); // click again while armed
|
||||
checkEq(sentFrames.join(","), "A0,R",
|
||||
"clicking again asks to disarm (A0) and releases every key (R) -- still no local flip");
|
||||
x.onFrame('S{"armed":0,"usb":1,"typing":0}');
|
||||
checkEq(x.armed, false, "and the disarm lands when the board says so");
|
||||
|
||||
sentFrames.length = 0;
|
||||
key("keydown", "KeyA");
|
||||
key("keyup", "KeyA");
|
||||
checkEq(sentFrames.length, 0,
|
||||
"while disarmed a whole press forwards nothing -- not even a restatement");
|
||||
|
||||
x.onFrame('S{"armed":1,"usb":1,"typing":0}'); // the board's own button arms it
|
||||
checkEq(x.armed, true, "a board-side arm is mirrored here without any click");
|
||||
|
||||
sentFrames.length = 0;
|
||||
key("keydown", "KeyA");
|
||||
checkEq(sframes().join(","), "S=00:04",
|
||||
"a press while armed restates the whole state: no modifiers, one usage (KeyA = 0x04)");
|
||||
key("keydown", "KeyA", { repeat: true });
|
||||
checkEq(sframes().length, 1,
|
||||
"autorepeat is not a state change -- the held state already says the key is down");
|
||||
key("keydown", "KeyB");
|
||||
checkEq(sframes().slice(-1)[0], "S=00:04,05",
|
||||
"the next restatement carries BOTH usages in press order, not an event for B alone");
|
||||
key("keyup", "KeyA");
|
||||
checkEq(sframes().slice(-1)[0], "S=00:05",
|
||||
"a release is a full restatement too: what is still held, nothing about A");
|
||||
key("keydown", "ShiftLeft");
|
||||
checkEq(sframes().slice(-1)[0], "S=02:05",
|
||||
"a modifier enters the mods byte (Shift = 0x02) and the held usage survives it");
|
||||
key("keyup", "ShiftLeft");
|
||||
checkEq(sframes().slice(-1)[0], "S=00:05", "and leaves it the same way");
|
||||
key("keyup", "KeyB");
|
||||
checkEq(sframes().slice(-1)[0], "S=00:",
|
||||
"the EMPTY restatement travels -- keys-up is a state, not an absence of event");
|
||||
|
||||
sentFrames.length = 0;
|
||||
key("keydown", "KeyA", { target: { tagName: "TEXTAREA" } });
|
||||
checkEq(sframes().length, 0,
|
||||
"a keystroke in the page's own text box belongs to the page, not to the target");
|
||||
key("keydown", "ShiftLeft");
|
||||
checkEq(sframes().slice(-1)[0], "S=02:", "set-up: Shift went down outside the field");
|
||||
key("keyup", "ShiftLeft", { target: { tagName: "TEXTAREA" } });
|
||||
checkEq(sframes().slice(-1)[0], "S=00:",
|
||||
"a modifier released OVER the page's field is still let go -- releasing Shift " +
|
||||
"above the text box must not strand it held on the target");
|
||||
|
||||
key("keydown", "KeyA"); // hold a key, then panic
|
||||
sentFrames.length = 0;
|
||||
key("keydown", "Escape", { ctrlKey: true, altKey: true });
|
||||
checkEq(sentFrames.join(","), "R,A0",
|
||||
"Ctrl+Alt+Esc releases every key (R) before disarming (A0), in that order");
|
||||
checkEq(x.armed, false, "and the page lands disarmed with the keys reset");
|
||||
|
||||
check(stateWire.length > 8 &&
|
||||
stateWire.every(f => /^S=[0-9a-f]{2}:(?:[0-9a-f]{2}(?:,[0-9a-f]{2})*)?$/.test(f)),
|
||||
"every state frame this section emitted is the wire form the firmware parses " +
|
||||
"(S=<hex mods>:<hex usages>) -- test_proto owns that parser");
|
||||
|
||||
console.log("responsive layout");
|
||||
// Byte-level layout claims. This suite is the only place the page can be seen
|
||||
// without a browser, and missing CSS fails silently -- it just renders the
|
||||
// laptop layout onto a phone. Each check names the mechanism it pins, so a
|
||||
// green run says the mechanism is in the served bytes, not that somebody felt
|
||||
// good about a breakpoint.
|
||||
function mediaBlock(query) {
|
||||
const at = served.indexOf("@media " + query);
|
||||
if (at < 0) return null;
|
||||
const start = served.indexOf("{", at);
|
||||
let depth = 0;
|
||||
for (let i = start; i < served.length; i++) {
|
||||
if (served[i] === "{") depth++;
|
||||
else if (served[i] === "}" && --depth === 0) return served.slice(start, i + 1);
|
||||
}
|
||||
return null; // unbalanced braces: report as absent
|
||||
}
|
||||
|
||||
const viewportMeta =
|
||||
(served.match(/<meta name="viewport" content="([^"]*)"/) || [])[1] || "";
|
||||
check(viewportMeta.includes("width=device-width"),
|
||||
"the viewport sizes to the device instead of a fake desktop canvas");
|
||||
check(viewportMeta.includes("viewport-fit=cover"),
|
||||
"the viewport meta carries viewport-fit=cover -- without it env(safe-area-inset-*) " +
|
||||
"is always 0 and the home indicator/notch sits on top of the page");
|
||||
check(!/maximum-scale|user-scalable=no/.test(viewportMeta),
|
||||
"and it does not disable pinch zoom -- a thumb floor must not be paid for with zoom");
|
||||
|
||||
const narrow = mediaBlock("(max-width:720px)");
|
||||
check(narrow !== null, "the one 720 px breakpoint exists");
|
||||
check(narrow !== null && /\.grid\s*\{[^}]*grid-template-columns\s*:\s*1fr/.test(narrow),
|
||||
"at the breakpoint the card grid is forced to one column (the base rule keeps " +
|
||||
"auto-fit/minmax(240px,1fr) for the laptop)");
|
||||
check(narrow !== null && /main\s*\{[^}]*display\s*:\s*flex[^}]*flex-direction\s*:\s*column/.test(narrow),
|
||||
"and main becomes a flex column there -- position:sticky on a grid item is pinned " +
|
||||
"inside its own grid area and never sticks, so this display switch is what makes " +
|
||||
"the ARM bar sticky for real");
|
||||
check(narrow !== null &&
|
||||
/input\[type=password\]\s*,\s*input\[type=text\]\s*,\s*textarea\s*\{[^}]*font-size\s*:\s*16px/.test(narrow),
|
||||
"inputs and the text box step to 16 px below the breakpoint -- iOS Safari zooms " +
|
||||
"the page on focusing a smaller field and never zooms back out");
|
||||
check(narrow !== null && /#log\s*\{[^}]*max-height\s*:/.test(narrow),
|
||||
"the log caps its own height on a phone instead of pushing the ARM bar off screen");
|
||||
|
||||
const coarse = mediaBlock("(pointer:coarse)");
|
||||
check(coarse !== null, "a coarse-pointer query exists for the thumb floor");
|
||||
check(coarse !== null &&
|
||||
/button\.k[^{}]*\.pill[^{}]*input[^{}]*summary\s*\{[^}]*min-height\s*:\s*44px/.test(coarse),
|
||||
"buttons, pills, inputs AND the folded notes' summary get a 44 px height floor on " +
|
||||
"touch devices; the fine-pointer page is untouched");
|
||||
check(coarse !== null && /min-width\s*:\s*44px/.test(coarse),
|
||||
"and narrow targets (chord buttons, checkboxes) get the matching width floor");
|
||||
// Why summary is in that list rather than a nice-to-have: task 02 moved five safety
|
||||
// sentences behind a <details>, which made the summary the only way to read them on a
|
||||
// phone. Its natural height is 17 px -- a tap target a thumb does not hit, in a build
|
||||
// whose byte-level layout assertions would otherwise still all have been green. The
|
||||
// rendered height is measured in a real browser at 360 px (.agents/HW_VERIFICATION.md);
|
||||
// this pins that the rule the measurement depends on is still in the served bytes.
|
||||
check(coarse !== null && /summary\s*\{[^}]*min-height\s*:\s*44px/.test(coarse) &&
|
||||
/summary\s*\{[^}]*padding\s*:/.test(coarse),
|
||||
"the fold's summary is a thumb-sized target, not the 17 px line of text it is by " +
|
||||
"default -- it is the only route to the folded safety notes");
|
||||
|
||||
check(/\.arm\s*\{[^}]*position\s*:\s*sticky[^}]*top\s*:\s*0[^}]*z-index/.test(served),
|
||||
"the ARM bar is sticky at the top of the scroll, above the cards -- the " +
|
||||
"armed/disarmed state must survive any scroll position");
|
||||
check(/#log\s*\{[^}]*overscroll-behavior\s*:\s*contain/.test(served),
|
||||
"scrolling the log does not scroll the page out from under the ARM bar");
|
||||
check(/padding-bottom\s*:\s*calc\(14px \+ env\(safe-area-inset-bottom\)\)/.test(served),
|
||||
"main's bottom padding carries the safe-area inset, so the last card clears the " +
|
||||
"home indicator (viewport-fit=cover above is what makes env() mean anything)");
|
||||
check(/button\.k\s*\{[^}]*touch-action\s*:\s*manipulation/.test(served),
|
||||
"controls set touch-action:manipulation -- no 300 ms double-tap-to-zoom between presses");
|
||||
check(/button\.k\s*\{[^}]*user-select\s*:\s*none/.test(served) &&
|
||||
/\.mousepad\s*\{[^}]*user-select\s*:\s*none/.test(served),
|
||||
"pressing a control or the pad does not start a text selection");
|
||||
check(/<main>\s*<button class="arm" id="arm"/.test(served),
|
||||
"the ARM bar is still the first element in main -- on every screen it is the " +
|
||||
"first thing below the header, no horizontal scroll assumed");
|
||||
const narrowKv = narrow !== null && /\.kv\s*\{[^}]*display\s*:\s*flex[^}]*flex-wrap/.test(narrow);
|
||||
check(narrowKv,
|
||||
"the State card's counters collapse to a wrapped summary at the breakpoint " +
|
||||
"instead of eight stacked rows -- they stay visible because they are the " +
|
||||
"'is it typing' evidence, only shorter");
|
||||
check(narrow !== null && /\.rowhead\s*\{[^}]*gap\s*:\s*\d+px/.test(narrow),
|
||||
"and the Log card's option row tightens so the card is two lines on a phone: " +
|
||||
"the options, then the log");
|
||||
|
||||
console.log("the safety statements that must never be tidied away");
|
||||
// Every long hint on this page exists because somebody got hurt without it, and a
|
||||
// page that reads as a wall of text invites exactly the edit that removes one. So
|
||||
// each statement is pinned by the words the operator has to read, BEFORE the prose
|
||||
// is folded -- the same assertions have to pass on the untrimmed page (they prove
|
||||
// the sentences were there) and on the trimmed one (they prove the fold kept them).
|
||||
//
|
||||
// They match text, not markup: tags stripped, entities decoded, whitespace
|
||||
// collapsed, case folded. That is what lets a sentence survive a rewrap and
|
||||
// survive moving inside a <details>, while still failing the moment it is deleted.
|
||||
// The matcher reads the served markup string rather than the shim's element tree --
|
||||
// <details> needs no shim, and a safety note one tap away is still on the page as
|
||||
// long as it is in the bytes the board serves. Anything matched here from the
|
||||
// script alone would also match a comment, so the script's safety statements are
|
||||
// pinned against `pageScript` explicitly instead.
|
||||
const decode = s => s
|
||||
.replace(/&#x([0-9a-f]+);/gi, (_, h) => String.fromCodePoint(parseInt(h, 16)))
|
||||
.replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d)))
|
||||
.replace(/—/g, "\u2014").replace(/–/g, "\u2013")
|
||||
.replace(/’|‘/g, "'").replace(/”|“/g, '"')
|
||||
.replace(/·/g, "\u00b7").replace(/ /g, " ")
|
||||
.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"')
|
||||
.replace(/&/g, "&");
|
||||
const pageTextOf = html => decode(html
|
||||
.replace(/<script[\s\S]*?<\/script>/gi, " ") // pinned against pageScript instead
|
||||
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
||||
.replace(/<!--[\s\S]*?-->/g, " ")
|
||||
.replace(/<[^>]+>/g, " ")).replace(/\s+/g, " ").trim().toLowerCase();
|
||||
const PAGE = pageTextOf(served);
|
||||
const says = (...phrases) => phrases.every(p => PAGE.includes(p));
|
||||
|
||||
// (1) the arming banner -- the one paragraph that must never sit behind a tap.
|
||||
const bannerHtml =
|
||||
(served.match(/<div class="banner" id="banner">([\s\S]*?)<\/div>/) || [])[1] || "";
|
||||
const bannerText = pageTextOf(bannerHtml);
|
||||
check(bannerText.includes("every key"),
|
||||
"the arming banner says EVERY key this tab presses goes to the target");
|
||||
check(/\benter\b/.test(bannerText),
|
||||
"and it names Enter by name -- the key that commits, the one nobody expects to travel");
|
||||
check(bannerText.includes("the machine the board is plugged into"),
|
||||
"and it says which machine receives them (not the one you are typing on)");
|
||||
check(/only on a machine you own|explicitly allowed to type on/.test(bannerText),
|
||||
"and it keeps the permission rule: a machine you own or may type on");
|
||||
const mainAt = served.indexOf("<main");
|
||||
const firstDetails = served.indexOf("<details", mainAt);
|
||||
const bannerAt = served.indexOf('id="banner"', mainAt);
|
||||
check(bannerAt > 0 && (firstDetails < 0 || bannerAt < firstDetails),
|
||||
"the arming banner is not inside or after a <details> -- no safety warning about " +
|
||||
"arming is ever one tap away from being unseen (holds with no folds at all)");
|
||||
|
||||
// (2) disarmed is the default, and what puts it back.
|
||||
check(says("nothing is forwarded while disarmed"),
|
||||
"the page still states the default: disarmed, nothing forwarded");
|
||||
check(says("reload", "focus loss", "socket loss"),
|
||||
"and still names what disarms you: reload, focus loss, socket loss");
|
||||
|
||||
// (3) the pointer: Esc is the rollback, and it releases.
|
||||
check(says("esc frees the cursor and lets go of every button"),
|
||||
"the mouse note still says Esc frees the cursor AND lets go of every button");
|
||||
check(says("never reaches the target"),
|
||||
"and that Esc belongs to the browser, so the pointer's rollback cannot be typed at the target");
|
||||
check(says("cannot leave one held down"),
|
||||
"and that restating the button byte every frame is what stops a lost frame sticking one");
|
||||
|
||||
// (4) a run of pasted text is cancelable, out loud.
|
||||
check(says("abandons the rest of a run"),
|
||||
"the send-text note still says stop typing / Ctrl+Alt+Esc abandons the rest of a run");
|
||||
|
||||
// (5) characters are positions.
|
||||
check(says("characters are positions", "us layout"),
|
||||
"the layout caveat is still on the page: positions, so the target must be on a US layout");
|
||||
|
||||
// (6) nothing stored, scheduled or replayed.
|
||||
check(says("no history", "nothing is saved"),
|
||||
"the send-text note still says nothing is stored, kept or typed later");
|
||||
check(says("nothing scheduled"),
|
||||
"and the mouse note still says nothing is scheduled (no macro, no saved gesture)");
|
||||
|
||||
// (7) the default-passphrase shout is the board's to raise, never static text: a
|
||||
// sentence in the markup would shout on a board that has a real passphrase and go
|
||||
// quiet on one that does not. It replaces the arming banner's own text, so it is
|
||||
// unmissable and it cannot be folded away with the hints.
|
||||
check(/if \(s\.dflt\)[\s\S]{0,400}default passphrase/i.test(pageScript),
|
||||
"the default-passphrase shout exists and is driven by the board's dflt flag");
|
||||
check(/if \(s\.dflt\)[\s\S]{0,160}\$\("banner"\)\.innerHTML/.test(pageScript),
|
||||
"and it lands on the arming banner, the one place that is never behind a tap");
|
||||
check(!PAGE.includes("default passphrase"),
|
||||
"and no static default-passphrase sentence hides in the markup -- the board raises it or nobody does");
|
||||
|
||||
// (8) a takeover cannot arm (phase 03). Guarded here so a later prose fold on the
|
||||
// Connection card cannot drop the sentence that says so.
|
||||
check(says("cannot arm it by doing so", "every handover lands disarmed"),
|
||||
"the Connection card still says taking the bridge cannot arm it -- every handover lands disarmed");
|
||||
|
||||
// (9) the clear-loaded-page warning (phase 02) -- hidden until true, but present.
|
||||
check(says("this page arrived in the clear"),
|
||||
"the clear-page warning is still in the served bytes");
|
||||
check(says("reload over"), "and it names the fix rather than just the fear");
|
||||
|
||||
// (10) Ctrl+Alt+Del is the one chord that can reboot a target, and its warning is
|
||||
// said at press time (the tooltip is a tooltip, not a manual, so this is where the
|
||||
// sentence has to live).
|
||||
check(/virtual terminal this REBOOTS the target/.test(pageScript),
|
||||
"the Ctrl+Alt+Del reboot warning is still said when the confirm is armed");
|
||||
|
||||
console.log("the folded notes are folded, not deleted");
|
||||
// Task 02 moved five paragraphs behind <details>. A fold is only safe while
|
||||
// there IS something behind it, so each one is checked structurally: the count
|
||||
// is pinned (deleting a whole note is a content decision, not a tidy-up, and it
|
||||
// must fail a suite), every fold names what is behind the tap, and every fold
|
||||
// still carries sentences. The safety checks above prove WHICH sentences survive;
|
||||
// these prove the containers did not hollow out.
|
||||
const folds = [...served.matchAll(/<details\b[^>]*>([\s\S]*?)<\/details>/gi)]
|
||||
.map(m => m[0]);
|
||||
checkEq(folds.length, 5,
|
||||
"the page carries exactly the five folded notes (chords, passphrase, mouse, " +
|
||||
"run, shortcuts) -- one fewer is a deleted note, not a tidied one");
|
||||
const summaries = folds.map(f =>
|
||||
(f.match(/<summary>([\s\S]*?)<\/summary>/i) || [])[1] || "");
|
||||
check(summaries.every(t => t.trim().length > 0),
|
||||
"every fold opens with a summary that names what is behind the tap -- a bare " +
|
||||
"<details> renders as 'Details', content one guess away");
|
||||
for (const want of ["why these are buttons", "what the passphrase does and does not buy",
|
||||
"how a forwarded mouse behaves", "what a run is and is not",
|
||||
"shortcuts while armed"]) {
|
||||
check(summaries.join(" | ").toLowerCase().includes(want),
|
||||
`the folded note "${want}" is still one of the page's five`);
|
||||
}
|
||||
folds.forEach((f, i) => {
|
||||
const body = pageTextOf(f.replace(/<summary>[\s\S]*?<\/summary>/i, " "));
|
||||
check(body.length >= 120,
|
||||
`fold ${i + 1} (${summaries[i] || "?"}) still carries its sentences `
|
||||
+ `(${body.length} chars) -- a folded note is not a deleted one`);
|
||||
});
|
||||
|
||||
console.log("the page's constants are the board's constants");
|
||||
// Hand-copied numbers, so they are checked against config.h here too: three readers
|
||||
// of the same arithmetic (here, test_proto.cpp, test_relayctl.py), none of them
|
||||
// trusting the page to have kept its own copy honest.
|
||||
const configText = fs.readFileSync(path.join(__dirname, "..", "src", "config.h"), "utf8");
|
||||
const configNum = name =>
|
||||
Number((configText.match(new RegExp("\\b" + name + "\\s*=\\s*(\\d+)")) || [])[1]);
|
||||
checkEq(x.TEXT_FRAME_MAX, configNum("kTextFrameMax"),
|
||||
"TEXT_FRAME_MAX is still config.h's kTextFrameMax -- drift here is an E8 mid-paste");
|
||||
checkEq(x.TEXT_JOB_MAX, configNum("kTextJobCap"),
|
||||
"TEXT_JOB_MAX is still config.h's kTextJobCap");
|
||||
|
||||
console.log("element ids");
|
||||
const markup = served;
|
||||
const present = new Set();
|
||||
@@ -747,6 +1089,15 @@ checkEq(!!x.inPageField({ target: null }), false, "a missing target does not thr
|
||||
const missing = [...requestedIds].filter(id => !present.has(id)).sort();
|
||||
checkEq(missing.join(","), "",
|
||||
"every element the script reached for exists in the page");
|
||||
// Prove this check bites. The shim hands out an element for ANY id, so a
|
||||
// mistyped id is invisible everywhere except right here -- which means a broken
|
||||
// matcher would also read as green. Hand it an id that cannot be in the markup
|
||||
// and require the same comparison to name exactly that one.
|
||||
byId("zzCoverSelfTest__notAnElement");
|
||||
const control = [...requestedIds].filter(id => !present.has(id));
|
||||
checkEq(control.join(","), "zzCoverSelfTest__notAnElement",
|
||||
"and the id-coverage comparison itself works: an id missing from the markup " +
|
||||
"is named by it (the shim would never have noticed)");
|
||||
|
||||
console.log(`page ${checks} checks, ${failures} failures`);
|
||||
process.exit(failures ? 1 : 0);
|
||||
|
||||
Reference in New Issue
Block a user