19 KiB
Phase 8D.6 Implementation
Status and Scope
Implemented, host-tested, build-verified and validated by explicit user sign-off on 2026-09-06. Prior 8D.5 sign-off stands. Independent final re-review confirmed both session-isolation and fit-readiness findings resolved. The sign-off below supersedes historical target-pending notes in this record. Numeric reserves remain open; M2 is not yet complete.
The starting worktree was clean. Production changes are confined to authored src/web_ui.c; focused tests extend tests/web_ui_session/. No server/protocol/settings/8D.7 policy change, generated asset regeneration, commit, upload or erase. The change fits the work-unit guideline without a preparatory split.
Target Sign-off and Evidence (2026-09-06)
After providing 60-second fresh-boot, full-client-mix and partial-cleanup telemetry, the user confirmed the remaining validation checks: "Yepp, that checks all out. Let's wrap up for the next phase." This closes 8D.6, including the Open admin fix and toolbar-order follow-up. The confirmation covers the discussed switching/lease preservation, console/connection isolation, browser/session recovery, layout and soak/recovery checks. Exact repetition counts, soak duration, browser versions/origin and flashed revision were not separately supplied; do not manufacture those details or treat missing per-case transcripts as blockers to this explicit closure. No target checks were executed by the agent.
| Resource (bytes) | Boot free / minimum / largest | Full mix free / minimum / largest | Partial cleanup free / minimum / largest |
|---|---|---|---|
| Internal 8-bit | 70,988 / 66,536 / 31,744 | 33,900 / 6,516 / 24,576 | 61,296 / 6,516 / 31,744 |
| Internal DMA | 63,232 / 58,780 / 31,744 | 26,144 / 1,580 / 24,576 | 53,540 / 1,580 / 31,744 |
| External PSRAM | 8,246,364 / 8,242,552 / 8,126,464 | 8,087,468 / 8,063,428 / 7,995,392 | 8,186,424 / 8,063,428 / 7,995,392 |
- Boot: HTTPS/SSH/mDNS and both web transports ready without reported startup errors; no sessions/broker clients, UART stopped, USB host closed. Configuration 230400 8N1/no flow. SSH owner minimum-free stack 18,472 B, configured 20,480 B.
- Full mix: UART running at 230400 baud; web broker 24 sole writer, SSH 9, second web 10, USB 11 observers. Two cookie sessions, two serial sockets, browser admin and admin SSH active concurrently. SSH stack minimum-free 16,280 B.
- Browser admin: five connections/four disconnections, six tickets issued/five consumed, 14 RX/1,096 TX bytes; zero reported capacity, authorization, protocol, input-backpressure, send or queue failures. Serial web: four connections/two disconnections, 47 RX/869,682 binary TX bytes, no transport failures; one expired ticket. HTTPS totals include two starts/one stop, two authentication failures and no response errors. These are cumulative observations, not attribution to particular validation actions.
- SSH: two successful handshakes and one unexplained handshake failure; 12 RX bytes, 11 accepted, one rejected, 47,392 TX bytes, no I/O failures. Observer input rejection is a possible explanation, not an established diagnosis.
- Cleanup is described as disconnecting all clients/admins except serial; only heap data was supplied. It is not a verified zero-client/UART-stopped baseline or a precisely timed cleanup sample. Internal/DMA largest blocks recovered to boot size. A single cleanup sample does not establish leak freedom or exact per-socket cost.
The 6,516 B internal / 1,580 B DMA lifetime minima remain a resource follow-up for 8D.7/M2. These capabilities overlap and minima are conservative sums across regions, not necessarily simultaneous free-space readings. Do not infer exhaustion, approve numeric reserves or claim an unexplained reserve-floor violation from these values alone. Preserve this evidence for comparable full-load/cleanup measurement in the next phase; it does not reopen user-approved 8D.6.
Toolbar follow-up moved Open/Close admin before the Serial/Admin group, keeping the selector rightmost. Production-renderer/CSP checks, all 17 UI groups, and git diff --check passed; bounded pio run passed in 7.60 s, unchanged 95,580 B RAM / 1,646,489 B flash. No JavaScript behavior or generated assets changed. This sign-off/handoff update itself is documentation only; no new build, upload or commit.
Next: separately requested 8D.7 — Web-shell lifecycle parity and M2 acceptance. Review and close the explicit unsupported self-affecting command restrictions through bounded deferred owner actions; preserve UART0 recovery, single dispatch, serial isolation and safe credential handling. Split if needed. Do not implement settings or declare M2 complete; no 8D.7 implementation is authorized by this wrap-up.
Open Admin Bug Follow-up (2026-09-06)
User reports working serial and mode selection, but Open admin displays Admin connection failed. Open admin to retry. Source diagnosis: web_ui.c:openAdmin() incorrectly validated a 32-character URL-safe serial ticket; web_admin_tickets.c:web_admin_tickets_issue() emits 32 random bytes as 64 lowercase hex characters. A successful admin-ticket POST was rejected locally before /ws/admin construction/admission. No Origin, role, shared-console, fit, subprotocol or private-adapter change is needed for this demonstrated failure. Repeated attempts can leave the two unconsumed tickets occupied until their 30-second expiry.
Changed only the admin validator to the backend's emitted format. The UI harness had incorrectly shared the serial fixture with admin; separate 64-hex admin responses now exercise the exact query URL, plus malformed/serial-format rejection and serial isolation. The corrected fixture failed before the source fix (only the serial socket existed), then all 17 UI groups passed. Production C renderer verifies the unchanged inline loader against the exact reported sha256-o6St1XqFiWgZZKDDKYP8Y1ROJxvOnf96z55w4i/dC20= CSP. The other reported UUID/index.js hashes are not that loader; their source is not established, and CSP was not relaxed.
Final focused reruns: python3 tests/web_ui_session/run.py, python3 tests/web_cookie_auth/run.py --admin (real store/ticket/endpoint-to-101 admission), python3 tests/web_admin_transport/run.py --tickets (19/12 groups), and python3 tests/web_admin_transport/server_lifecycle.py (11 groups) all pass. Frontend request behavior and backend admission were tested in separate harnesses, not an end-to-end browser/network session. Final bounded pio run passed in 7.96 s, 95,580 B RAM / 1,646,489 B flash, unchanged from the previous build. git diff --check passes. No assets, uploads, commits or target execution. Parent review and user target retest remain pending; this bug fix is not a new phase or sign-off. On next user-controlled deployment, reload the app and verify explicit Open admin reaches a prompt while serial remains connected; no ticket/cookie/CSRF values are needed in any report.
Browser Contract
- Validated
/api/sessionrole reveals Serial/Admin selection only for admins. Ordinary users retain serial-only navigation; existing server authorization remains authoritative. - Selecting Admin lazily creates one additional xterm instance. Open admin explicitly POSTs
/api/admin/ws-ticketwith the existing CSRF/cookie policy, then connects/ws/admin. Selection alone does not open a socket or run a command. Failures and shell termination require explicit reopen, not automatic command/session replay. - Mode switches only change visibility, focus, selected-terminal sizing and input gating. Both connected sockets continue receiving output. The same serial socket, broker identity and writer lease remain; broker status and Request control/Release control remain visible in both modes.
- Serial and admin have separate 5,000-line scrollbacks and separate 65,536-byte pending xterm-write limits, released through write callbacks. Excess frames are discarded while the socket continues draining, with cumulative per-terminal browser-drop byte counts visible in both modes. The UI explicitly states oldest scrollback lines expire. These counts are not firmware broker-drop telemetry or a byte-integrity guarantee.
- Keyboard/paste goes only to the selected terminal. Serial retains its writer check and 1,024-byte framing; admin sends binary frames of at most 512 bytes with a 4,096-byte input event/socket-buffer admission limit. Excess admin input closes only the shell rather than silently truncating a command. Earlier accepted input cannot be rolled back.
- Close admin, shell
exitand admin failures do not disconnect serial. Explicit serial Disconnect still closes only serial and pauses its reconnect. Admin open has a 15-second handshake deadline in addition to the existing 15-second API deadline. Capacity/security errors remain safe-text/manual retry; server admission errors during upgrade appear as generic connection failure. - Logout, session expiry/401 and pagehide cancel pending work and close both routes. Generation guards reject late responses/callbacks; socket handler properties are cleared before close. Resize listeners/observer are removed on exit and restored once for bfcache. Restored pages revalidate the session and preserve serial pause policy; admin requires explicit reopen. At most two terminals and one input subscription each persist for the page lifetime, not one per switch/reconnect.
- Scrollback belongs to the document's first validated username/role/session-stable CSRF tuple, retained separately from the active request CSRF value. Before adopting any later session view, a mismatch hides both terminals, closes both routes, fences pending work and replaces the document at
/. This also prevents logout from mutating a replacement session. Pagehide hides both terminal hosts and blocks selection until successful same-session validation; same-session restore and mode switches retain scrollback. Failed or delayed restore validation never reveals the retained buffers. - Terminal bounds are cached only after valid dimensions and successful resize (or an already matching size). An unavailable/invalid measurement gets at most three animation-frame retries per external fit request. Work cancellation removes the pending frame, resets the retry budget and generation-fences stale callbacks; teardown/logout cannot restart readiness retries. A later resize/selection can explicitly start a fresh bounded attempt.
- Existing dark panel/button visual language is retained, selected buttons expose
aria-pressed, and narrow layouts can scroll rather than clipping the terminal below the dashboard. Real desktop/mobile rendering remains to be checked. The inline loader and its exact CSP hash are unchanged and tested; authored app/document remain no-store.
Executed Validation
All commands ran successfully in this workspace:
| Command | Result |
|---|---|
python3 tests/web_ui_session/run.py |
Production C renderer/header/failure/CSP checks and 16 Node groups pass |
python3 tests/web_login_ui/run.py |
C/HTML checks and eight Node groups pass |
python3 tests/web_cookie_auth/run.py --admin |
Cookie/session policy plus combined real store/ticket/admin endpoint tests pass |
python3 tests/web_auth_parse/run.py |
268 cases, zero failures |
python3 tests/web_session_store/run.py --serial |
Store and serial binding/isolation/races pass |
python3 tests/web_admin_transport/run.py --tickets |
19 transport groups and 12 ticket groups pass |
python3 tests/web_admin_transport/server_lifecycle.py |
11 lifecycle groups pass |
python3 tests/admin_console_boundary/run.py |
Shared-owner/currentness/prompt/completion/SSH boundary suites pass |
pio run |
Final source build after review fixes succeeds in 21.35 seconds, 120-second finite timeout |
git diff --check |
Pass |
New UI checks cover 20 switch cycles with unchanged serial socket/client/writer IDs, hidden binary output and independent terminals, selected input framing, user-only navigation, visible overflow and resumed draining, close/reopen/remote-close isolation, 401/logout/pagehide/expiry, cancelled admin ticket late 401, stale socket callbacks, selected resize, three bfcache listener cleanup cycles and handshake timeout. The harness executes production C-rendered JavaScript with DOM/xterm/WebSocket/timer doubles. It does not execute real terminal escape parsing, hidden prompts, CSS layout, browser CSP enforcement, TLS/HTTPD scheduling, target UART traffic or browser heap profiling.
Independent Review Fixes (2026-09-06)
Both reported P2 findings are fixed, with parent re-review pending:
- Cross-session retained scrollback/live admin:
loadSessionnow compares identity before adopting CSRF, role, expiry or view. It requires a clean document on mismatch rather than reusing A's buffers or live admin socket under B's cookie. Regression coverage checks admin-to-admin, admin-to-user, same-account/new-CSRF, username-only and role-only changes, each during active restore, paused restore, live serial reconnect with admin still open, and logout. Tests assert clean/replacement, hidden old terminals, both sockets closed, no new ticket/logout request, unchanged old session view and fenced callbacks. Same-identity restore retains both scrollbacks and live same-session reconnect retains admin. - Failed initial fit cached forever: Valid measurements alone populate the bounds cache; three bounded readiness retries recover from an initially undefined measurement at identical host bounds. Tests also exhaust retries, verify later external retry, and invoke stale callbacks after pagehide/restore, expiry and logout. The first test run exposed a fresh fit scheduled by logout's session check; suppressing fits while logging out fixed that case before the final passing rerun/build.
After the fixes, reran tests/web_ui_session/run.py (16 groups), tests/web_login_ui/run.py (8 groups), tests/web_cookie_auth/run.py --admin, tests/web_session_store/run.py --serial and tests/web_auth_parse/run.py (268 cases), all with python3, all passing. The other unchanged-backend suites in the table passed during initial implementation and were not rerun for these UI-only review fixes. Final pio run and git diff --check pass. No target/browser result, phase sign-off or M2 completion is claimed.
Resource Accounting
Final PlatformIO espressif32 6.12.0 / ESP-IDF 5.5.0 N16R8 release build:
| Metric | Final | Versus signed-off 8D.5 build | Versus recorded 8D.0 |
|---|---|---|---|
| Linked RAM | 95,580 B | 0 B | +1,048 B |
| Flash | 1,646,489 B | +9,216 B | +46,516 B |
The delta is authored HTML/CSS/JavaScript read-only content. Review fixes add 0 B RAM / 1,376 B flash versus the initial 8D.6 build (22.22 seconds, 95,580 / 1,645,113 B). An earlier pre-final-CSS build passed in 25.30 seconds at 95,580 / 1,644,905 B. The table is the final post-review-fix source result. Browser-only additions are one retained session identity tuple and a retry counter using the existing single animation-frame slot, not new firmware storage or a polling task.
No firmware runtime allocations, tasks/stacks, queues, request scratch, route count or capacities are added. Existing six HTTPS socket slots, two serial sockets, one optional admin socket, two admin tickets and two shared console slots are unchanged. The UI now exercises the already-allocated backend with simultaneous serial/admin WebSockets plus periodic HTTPS requests; actual loaded TLS/socket/console heap headroom still requires target measurement. There is no LRU eviction/capacity increase to make the selector work.
Browser cost adds a lazy terminal/fit addon and 5,000 lines of scrollback, plus at most 64 KiB pending output payload per terminal (128 KiB together) and callback/object overhead. This is not a total browser heap bound: xterm cell storage depends on dimensions, browser networking queues are implementation-owned, and JavaScript cannot securely wipe engine-managed strings. Numeric internal/DMA/largest-block/stack reserve gates remain open.
Target Checklist (Retained for Regression)
The user sign-off above closes 8D.6. This original checklist is retained for future regression coverage; individual unrecorded details are evidence limitations, not outstanding phase gates.
- Record exact flashed revision, browser versions and direct-IP/mDNS origin. Verify user serial-only login and admin selector on desktop and narrow/mobile screens, selected styling/focus, resize/orientation and usable terminal height. Exercise initially unavailable font/cell measurements: sizing must recover at unchanged bounds or after a later explicit resize, without unbounded retries.
- With serial writer active and sustained UART output, perform 20 Serial/Admin switches. Confirm unchanged broker client/writer IDs, Request/Release from both modes, continued hidden output and no admin keystrokes on UART1. Repeat as observer. Distinguish browser-drop counts, firmware broker drops and expected scrollback rollover.
- Exercise admin empty Enter, normal commands, history, Tab completion, Ctrl+C and hidden prompts while switching views. Confirm no input/output/prompt crossover with UART0/admin SSH. Close/reopen five times and use
exit/empty Ctrl+D; serial must remain intact. Explicit serial Disconnect/reconnect must not close admin. - Test admin capacity rejection with existing console/socket occupancy, network failure, stale ticket and repeated manual reopen; retained serial must not be evicted. Confirm existing 8D.5 unsupported self-affecting command restrictions still reject without side effects; no 8D.7 parity is claimed.
- Test logout, account revocation, absolute expiry, pagehide/bfcache restore, ordinary reload and interrupted logout with both routes open and with a pending ticket. No stale admin command/session replay; expired auth returns to usable login. Recheck unrelated-session isolation. Change the same-origin cookie via another tab/account login (admin-to-admin and admin-to-user), then restore/reconnect: no old scrollback may appear in the new document, and old admin must close. Same-session restore must retain both scrollbacks; delayed/failed revalidation must leave them hidden.
- Run UART0 recovery, USB UART1, user/admin SSH and two-browser serial plus one browser-admin full mix with HTTPS polling. Capture
memory, transport/broker counters and SSH stack margins at settled boot, serial-only, serial+admin, full load and after five open/close/logout cycles. Compare internal/DMA/PSRAM free/minimum/largest blocks without inventing reserve floors. - Run a bounded 15-minute mixed-client soak at the user's supported workload (record actual baud; prior 8D.5 was 230400), then disconnect and collect 60-second cleanup telemetry. Check no watchdog/stack faults, monotonic leaks, declining largest-block trend or unexplained lease loss. Profile browser memory across repeated switches and output overflow where feasible.
8D.6 is closed by user sign-off above. Wait for a separate 8D.7 implementation request; numeric reserves and the M2 acceptance milestone remain open.