# Phase 8D.18 — Client/writer contextual dialogs ## Status and scope (2026-09-13) User-authorized 8D.18 implementation complete; host tests and bounded firmware builds pass. **Independent review complete with no confirmed actionable defects; target/user sign-off remains pending.** Reviewer independently reran135 UI groups+renderer/HTML/CSP, canonical broker diagnostics/management/wrap, cookie Broker and diff checks PASS. Parent final build confirmation PASS6.89s,100,300 B RAM/1,793,301 B flash; diff check PASS. Native picker behavior during live option updates and real blank-to-target reselection remain target checks, not established by direct Node change events. No M3 or memory-reserve approval. Initial read-only Git status was empty; no preexisting work was removed. Only `src/web_ui.c`, `tests/web_ui_session/{broker.cjs,layout.py}` and phase/agent documentation changed. No backend, writer policy, API, dispatcher, task, queue, stack-size, schema, dependency, icon, generated asset, SDK or configuration changes. No 8D.19/later or removed 8D.15 diagnostics. No upload, erase, commit or branch creation. The signed-off CPU160 combined binary WebSocket send remains untouched. ## Implementation plan and completed source set 1. Read code-map, current-state, relevant architecture/design decisions and exact 8D.18 row; inspect 8D.17's single quick host/focus correction and 8D.16's typed Broker controller and lease-fence contract. 2. Establish clean-worktree/bounded-build baseline. Reuse the existing Broker snapshot, session validation and operation controller, not a second backend or UI owner. 3. Extend the existing status-trigger/nonmodal quick host to Broker clients and Active writer. Preserve terminal receive/input isolation, dismissal focus semantics and full-page draft ownership. 4. Introduce one explicit selection record and single-flight, deadline-bounded live snapshot refresh. Never renew selection tokens implicitly; latch invalidation and retain visible stale-result/uncertainty messages. 5. Extend emitted-production-JS, parsed HTML and existing broker/auth/lifecycle regressions; build, verify CPU160 and record exact limits and pending target checks. All five steps completed. Shared firmware APIs already supplied the required correctness boundary; no owner adaptation was necessary. ## UI and ownership contract - Broker clients and Active writer status values are native labeled buttons, initially disabled, enabled only after validated admin session adoption. Ordinary users retain ordinary status values, cannot open management via hover/focus/click, and receive unchanged server-side denial on protected endpoints. No management read is issued for ordinary users. - Both contextual entrances disclose the **same existing Broker form** in the sole `serial-settings` host. They have distinct live-client/writer accessible titles, share the client list/selection/explicit assignment controls, and use the existing browser-native confirmation dialog for the actual transfer. No second draft, client list, operation slot or DOM clone. - Mouse hover and keyboard focus disclose without mutation; native click/Enter/Space/tap activation focuses Close. Touch pointer-enter alone does not disclose. `aria-controls`, `aria-expanded`, `aria-haspopup=dialog`, accessible names, visible focus, nonmodal dialog semantics, native select/actions, Close and full-page link remain available. No focus trap. - The 8D.17 250ms pointer/focus bridge, focused-editor protection, Escape/outside dismissal and stationary-pointer suppression apply to both new triggers. Dismissal/adjacent-trigger switching restores logical terminal selection without calling xterm focus; Close/Escape focus only the trigger. Both terminals continue receiving bounded binary-transparent output, with input disabled while the settings host is selected. No socket/client/lease mutation on UI navigation. - Full-page Settings retains its sole DOM/draft owner. **Neither hover, focus nor explicit activation of the new Broker/writer triggers changes a full-page domain or its draft**, including transient account/Network password input. Explicit activation focuses the existing domain navigation; trigger help explains using Broker navigation or returning to a terminal first. Existing Serial/Wi-Fi explicit full-page navigation is unchanged. Switching between quick views is deliberate context departure and retains existing draft cleanup rules. - Promotion through Open full settings page retains the same Broker selection/controller without refetch, abort, POST or replay; it stops contextual live scheduling. Full Broker remains manually refreshed. Closing/switching clears selection, aborts/fences browser reads/results and stops timers, but never claims cancellation of admitted firmware work. - Periodic button state updates use `aria-disabled` and preserve a currently focused control instead of natively disabling it and dropping keyboard focus. Nonfocused unavailable controls remain natively disabled. Independent request/action guards enforce availability, including clicks delivered to an aria-disabled focused button. Stale assignment attempts display a rejection without a POST. ## Live refresh and selection fences - Immediate opening read, then **one next snapshot refresh 5 seconds after successful completion**, only while a Broker contextual view is open. Each cycle revalidates the current login, then uses the existing bodyless GET `/api/settings/broker` (strict 2048-byte/eight-client schema). This is not an additional aggregate status backend. - At most one active Broker snapshot/operation controller. Manual reads, repeated opening and result/assignment work cannot overlap that controller. A **5-second whole snapshot-cycle deadline** covers session validation, fetch and body handling; timeout aborts, clears controller ownership and invalidates selection. The deadline is cleared on completion/abort. Error/timeout stops automatic live refresh until explicit Refresh/reopen; no automatic error retry. - No timer while an operation/result is active or its outcome is pending/unknown. Known terminal operation results use the existing refresh path. Polling frequency is not a hard wall-clock freshness guarantee: background timer throttling, HTTPD and network delays remain possible. - Explicit select change captures target ID, type/name identity and the current opaque lease generation. Reorder/counter-only refresh retains that choice and token. A lease version or identity mismatch latches `stale`; a failed/invalid/timed-out snapshot also latches it. Later matching data cannot clear the latch. A missing target clears the displayed select and latches absence so its later appearance cannot silently reselect it. - Refresh never authorizes a new transfer. The admin must deliberately choose the blank option then the target again to reselect an otherwise still-displayed stale target. Assignment stays blocked for missing/stale/exhausted/current-writer targets. Submission consumes validity; operation completion refresh cannot re-arm it. - Confirmation captures exact target/version **before** asynchronous session revalidation and submits once through `/api/settings/broker-operation`. Concurrent real lease changes after the browser snapshot are rejected by the unchanged atomic 8D.16 broker fence. Existing conflict result text visibly states no lease change by that operation. No check-unlock-force, lease-policy change or automatic mutation retry. - Existing bounded result checks remain at most 10 one-second GETs/15 seconds; lost ACK, replacement, invalid response, expiry and late responses retain uncertainty and explicit Check Result recovery. Browser abort is not server cancellation, successful execution is not a guarantee the writer remains current, and latest login-bound results are not durable/idempotent records. - Session/work generation, request-controller identity and API abort/current checks fence late responses, including late 401 after navigation. Expiry/logout/pagehide invalidate views and close both routes through existing cleanup. New views cannot adopt stale selection/snapshot/result state. ## Executed validation All commands below passed, bounded at 120 seconds per tool invocation; none timed out. Broad suites ran after the initial implementation; the final focused-control refinement was followed by another complete UI suite and firmware build. ```sh python3 tests/web_ui_session/run.py python3 tests/session_broker_diagnostics/run.py python3 tests/web_cookie_auth/run.py --broker python3 tests/admin_console_boundary/run.py python3 tests/web_admin_transport/server_lifecycle.py python3 tests/web_cookie_auth/run.py python3 tests/web_cookie_auth/run.py --settings python3 tests/web_cookie_auth/run.py --serial-settings python3 tests/web_cookie_auth/run.py --accounts python3 tests/web_cookie_auth/run.py --admin python3 tests/web_cookie_auth/run.py --network python3 tests/web_cookie_auth/run.py --display python3 tests/web_auth_parse/run.py python3 tests/web_session_store/run.py python3 tests/web_session_store/run.py --serial python3 tests/admin_console_boundary/accounts.py python3 tests/admin_console_boundary/lifecycle.py python3 tests/admin_ssh_policy/run.py python3 tests/web_network_settings/run.py python3 tests/web_admin_transport/run.py --tickets python3 tests/web_httpd_idle/run.py python3 tests/web_diagnostics/run.py python3 tests/web_serial_performance/run.py python3 tests/web_security/run.py python3 tests/web_login_ui/run.py pio run git --no-pager diff --check ``` Final UI **135 behavior groups** (128 baseline +7 contextual groups), plus actual C rendering/header/error checks, parsed unique HTML ownership/native labels and exact unchanged inline-loader CSP. New groups cover: - ordinary/admin pointer/focus/touch disclosure, explicit close/Escape/outside focus, Serial/Admin receive and input isolation; - deferred single-flight reads, reordered/counter-updated clients, preserved explicit choice, stale lease/identity latches and missing/returning target non-resurrection; - focused action/select accessibility during refresh and stale rejection without focus theft or POST; - malformed/503 snapshots, stalled session/snapshot deadlines, explicit recovery and ignored late snapshot data; - full-page Serial/account/Network drafts, adjacent Serial→writer keyboard focus and promotion without reads/reset; - captured confirmation through delayed session validation, visible conflict, lost acknowledgement, dismissal/reopen without replay; - late read/POST/result401 after adjacent quick navigation, current expiry/pagehide/principal replacement/401 cleanup. Existing canonical broker management/wrap tests verify real lease ABA/target-generation rejection; cookie Broker tests verify admin/session/Origin/CSRF/bounds/result isolation; lifecycle25 and unchanged dispatcher capacity tests pass. Parser294, transports/tickets, session binding, Network/accounts, idle, diagnostics, combined-send performance, security and login regressions also pass. Test-development failures were corrected rather than hidden: one old assertion expected refresh to clear the choice; new refresh fixtures initially fired ordinary status's 5-second interval instead of the dedicated one-shot, and a delayed session fixture used the wrong username. The final harness explicitly distinguishes interval versus one-shot timers and uses the actual same principal. Production strict identity rejection was correct. **Limits:** Node uses deterministic DOM/fetch/clock/socket doubles, not real native browser keyboard event defaults, focus engine, screen reader, mobile picker or touch hardware. Parsed native button semantics plus exercised click/focus/keydown/pointer paths do not establish real-browser accessibility sign-off. No optional Chromium geometry run in this phase; 8D.17 recorded sandbox launch restrictions. No on-device concurrency, actual TLS timing, heap/reserve, HTTPD/dispatcher stack or full-mix target measurement. These results do not reopen or expand the signed-off CPU160 throughput evidence. ## Build and resources | Build | RAM | Flash | Time | |---|---:|---:|---:| | Actual pre-edit baseline | 100,300 B | 1,788,661 B | 7.02 s | | Initial implementation | 100,300 B | 1,793,077 B | 21.99 s | | Final focused-control refinement | 100,300 B | 1,793,301 B | 21.52 s | | Final delta from baseline | **0 B** | **+4,640 B** | incremental times not comparable | CPU160 verified by targeted inspection of `sdkconfig.defaults`, active `sdkconfig.esp32-s3-devkitc-1-n16r8` and generated `.pio/build/esp32-s3-devkitc-1-n16r8/config/sdkconfig.h`. PlatformIO's generic board banner still says240MHz; actual config says160. No edits to these files. 33 handlers/six sockets, existing tasks/stacks/queue capacity and broker/WebSocket buffers are unchanged. Browser additions are one bounded selection record, one live scheduling timer or active read deadline, and two trigger listener sets; existing rows/controllers are reused. Regular live reads add HTTPS load while shown; runtime headroom remains unmeasured. ## Pending parent and target checklist - [x] Independent review of source, tests, ownership, stale selection and request/focus fencing; no confirmed actionable defects. - [ ] Desktop mouse hover bridge, scrollable client list, adjacent triggers, outside dismissal and no unwanted reopening after Escape. - [ ] Keyboard Tab/Shift-Tab, native Enter/Space, select navigation, blank/reselect, focused action refresh/stale invalidation, native confirmation Cancel/Accept and Escape; no terminal key leakage. Screen-reader titles/expanded/aria-disabled/status announcements. - [ ] Touch tap/scroll/select/confirm/outside tap, rotation/zoom/software keyboard and narrow/short viewport geometry; real native-picker behavior during refresh. - [ ] Every full settings draft survives Broker/writer activation; sole-host promotion and Serial/Wi-Fi quick switching behave as documented. - [ ] USB/SSH/two-browser clients join/leave/reorder; concurrent shell/tab writer changes and same-writer release/reacquire reject stale confirmation visibly; explicit reselection does not assign until separately confirmed. - [ ] Delayed/lost snapshot/ACK/results, actual network loss, expiry/logout/bfcache and identity replacement: no late state restoration, mutation replay or cancellation claims; Check Result recovery remains available. - [ ] Both browser terminals continue receiving with UI open; correct input focus after dismissal; physical UART0 administration/native USB recovery remain usable. - [ ] Full mix at signed-off160MHz/230400 baud with live refresh: capture drops/latency/heap and HTTPD/dispatcher margins, without inferring numeric reserve approval. No target checklist item or independent-review item was signed off by this implementation. Stop before 8D.19 or any other separately authorized phase.