Implements admin-only Display settings with generation-checked Apply, Save, Load, Defaults, and Reset operations across the web UI, CLI, SSH dispatcher, and local UI owner. Adds bounded HTTP handling, session-isolated operation results, browser lifecycle support, and comprehensive host tests and documentation.
139 lines
19 KiB
Markdown
139 lines
19 KiB
Markdown
# Phase 8D.14 — Typed Display settings
|
||
|
||
## Status and scope (2026-09-09)
|
||
|
||
User-authorized implementation complete; host-tested and production-build-verified. **Target validation and phase sign-off remain pending.** No 8D.15/later phase or full M3 claim.
|
||
|
||
Adds an admin-only **Settings → Display** view with Serial-style label/value presentation, typed dim/off inactivity edits, and explicit Apply/Save/Load/Defaults/Reset. The production starting worktree reported clean, despite the request's warning about uncommitted work; the existing combined binary WebSocket throughput fix and CPU160MHz configuration were preserved without source/configuration edits. Prior user full-mix230400 throughput acceptance stands and is not a validation claim for these Display changes.
|
||
|
||
No I2C/framebuffer/renderer ownership changes, electrical-diagnostic UI, secret fields/logging, generic command runner/job framework, new task/timer/mutex allocation, stack-size increase, queue-depth/item expansion, socket increase, NVS schema migration, generated-asset change/regeneration, upload, erase, branch or commit. Browser-shell authorization restrictions are unchanged.
|
||
|
||
## Production files and ownership
|
||
|
||
- `src/web_display_settings.{c,h}`: bounded HTTP authorization/admission, one login-bound operation/result slot, dispatcher execution adapter and RAM snapshot encoding.
|
||
- `src/local_status_ui.{c,h}`: authoritative configuration generation and zero-wait settings projection; conditional public configuration/persistence operations.
|
||
- `src/local_ui_console.c`: canonical CLI Set uses compare-generation Apply; Save/Load/Reset use the same reserved owner API. Defaults and existing public Apply also pass through the owner gate.
|
||
- `src/admin_ssh_console.{c,h}`: Display ID in the existing typed request union and existing sole dispatcher, without a command string.
|
||
- `src/web_server.c`, `src/CMakeLists.txt`: optional route registration and source composition.
|
||
- `src/web_ui.c`: authored Display HTML/JS; no vendored/generated asset edits or loader/CSP changes.
|
||
- `local_ui_config.c` remains the unchanged canonical version-1, 12-byte NVS validator/loader/saver.
|
||
|
||
### Configuration/currentness contract
|
||
|
||
`local_status_ui_get_settings(config, generation)` copies RAM plus a nonzero generation in one short timing critical section. It returns unavailable if UI startup did not publish configuration, or busy while a reserved configuration/storage operation is running. It never accesses the panel or NVS. The preexisting renderer/get-config path can still read the last committed RAM configuration during storage work.
|
||
|
||
`local_status_ui_update_settings(action, expected_generation, config, loaded_defaults)` compares generation and reserves configuration mutation under that same critical section. Storage IO runs **outside** the critical section. Every configuration writer, including legacy Apply and CLI, honors the reservation; competing mutations return busy rather than block. Browser requests require a nonzero expected generation; zero is reserved for unconditional canonical CLI operations. Successful RAM publication advances the generation even when bytes are identical, and signals external activity. Save does not advance generation or signal activity. Generation exhaustion fails closed rather than wrapping.
|
||
|
||
Buttons currently do **not** edit these timeouts: their activity, wake, navigation and diagnostic-hold sequences remain distinct from the configuration generation. They can proceed during NVS work. No timing lock is retained across I2C. The UI task is still optional and firmware-lifetime: configuration can work with an absent panel if the task starts, while unavailable task/button initialization yields an unavailable settings snapshot. No response claims actual brightness, successful I2C, physical presence or successful reprobe. Failed bus creation remains unrecoverable through the existing button reprobe.
|
||
|
||
### Persistence semantics
|
||
|
||
| Action | Required fields besides `action` | Effect |
|
||
|---|---|---|
|
||
| `apply` | `generation`, `dim_seconds`, `off_seconds` | Replace both RAM timeout values, not NVS; signal activity. |
|
||
| `save` | `generation` | Persist exactly the selected working generation; browser drafts are ignored. Reservation prevents changes during Save. |
|
||
| `load` | `generation` | Canonical load into RAM. Missing/incompatible/invalid storage selects defaults and returns `loaded_defaults`; NVS is not overwritten. IO errors leave RAM unchanged. |
|
||
| `defaults` | `generation` | Apply 300/600-second defaults to RAM only; signal activity. |
|
||
| `reset` | `generation` | Save defaults first, then publish RAM and signal activity. |
|
||
|
||
**Intentional CLI behavior refinement:** Reset previously applied RAM and attempted rollback on storage failure. Both typed and CLI Reset now commit defaults before RAM publication. Failure leaves RAM unchanged, so no rollback can overwrite concurrent activity/configuration and no `rollback_failed` result is needed. Successful resulting RAM/NVS values are unchanged. A storage error is not a power-loss guarantee about physical flash; inspect/reload after uncertain delivery or storage failure.
|
||
|
||
Both timeout values are unsigned integers in **0–86,400 seconds**. Zero disables that transition. If both are nonzero, Off must be strictly later than Dim. The fixed NVS version is supplied internally, not browser-editable. Set rejects stale read/modify/write configuration rather than overwriting a concurrent edit. No persisted generation, schema expansion or migration is needed.
|
||
|
||
## HTTP and operation contract
|
||
|
||
All three routes reuse current cookie/principal authentication, admin role enforcement, strict header/framing validation, no-store/nosniff/no-referrer response policy and request scratch cleanup. Normal users receive 403; invalid/expired sessions receive 401. GET is bodyless/queryless with the existing GET Origin policy. POST additionally requires canonical same-origin Origin and CSRF, a supported JSON Content-Type, no query or transfer encoding, and a nonempty body.
|
||
|
||
| Route | Method | Payload / bound |
|
||
|---|---|---|
|
||
| `/api/settings/display` | GET | Exactly `generation`, `dim_seconds`, `off_seconds`; 128-byte response buffer. 503 if UI unavailable/busy. No storage/panel reads. |
|
||
| `/api/settings/display-operation` | POST | Maximum 256 bytes and four receive calls. Exactly the fields in the action table. 202 returns admission, not completion. |
|
||
| `/api/settings/display-operation` | GET | Latest result for this originating login only, exactly `id`, `action`, `state`; 96-byte response buffer. |
|
||
|
||
Strict flat parser rejects unknown/duplicate/missing fields, escapes, strings in numeric fields, negative/fractional/exponent/overflow/leading-zero numbers, embedded NUL, trailing garbage, nested values and unsupported actions. Partial/error/excessively fragmented bodies are rejected, with unread bodies closing rather than unbounded drain/retry.
|
||
|
||
One static operation slot is shared by all administrators, but result retrieval is isolated by non-reused originating web-session ID. Other logins see `{id:0,action:"none",state:"idle"}`. Pending/full dispatcher admission returns 503 with Retry-After 1. Slot IDs never wrap; completed results are replaceable, not durable history or idempotent retry records.
|
||
|
||
Only an ID enters the existing four-entry dispatcher queue. On dequeue, the adapter validates the original session/principal/admin and **30-second admission deadline**, then calls the owner conditional API. This is the Serial-style dequeue deadline: **no new timer**, no hard cancellation, and a blocked dispatcher can retain this small secret-free pending record beyond 30 seconds until dequeue. Restart/revocation does not authorize stale queued work: session IDs are not reused. Already-admitted storage work can complete after logout, absolute expiry or server stop; acknowledgement/result delivery is not guaranteed.
|
||
|
||
States:
|
||
|
||
- `idle`: no result belonging to this login; not proof earlier work was cancelled.
|
||
- `pending`: queued or executing, not completed.
|
||
- `ok`: owner operation completed; only Save/Reset imply successful explicit persistence.
|
||
- `loaded_defaults`: Load selected defaults without overwriting missing/incompatible storage.
|
||
- `conflict`: selected generation stale, owner unavailable or generation exhausted; this operation made no change.
|
||
- `failed`: owner busy, validation/storage failure; this operation did not publish RAM. Inspect storage after uncertainty.
|
||
- `cancelled`: original session/currentness/dequeue deadline rejected before owner admission.
|
||
|
||
Optional registrations are staged through the existing adapter: snapshot first, then operation GET, then POST. A POST allocation failure unregisters the operation GET; failed unregister can leave a read-only result route, never a mutation-only domain. Failure preserves other settings/auth/transports. HTTPD handler budget **27 → 30**, six sockets/no LRU unchanged.
|
||
|
||
## Browser lifecycle
|
||
|
||
Display navigation preserves both sockets, hidden output draining, broker identity and writer lease; only terminal keyboard routing/view selection changes. It reads fresh working state on entry and allows explicit Refresh. Invalid/unavailable snapshots disable mutation until a valid refresh. Refresh discards drafts; there is no saved-value read or automatic mutation on selection.
|
||
|
||
All mutation bodies carry the last loaded generation, so a CLI edit between refresh and dequeue becomes a conflict, including Save. Routine Apply/Save/Load/Defaults use inline consequences; only Reset asks for confirmation of saved-NVS overwrite. Pending values remain visible but marked stale; repeated submission is disabled.
|
||
|
||
After a validated 202 acknowledgement, automatic completion checks run at one-second intervals, **at most ten GETs and fifteen seconds overall**, including session checks. Known terminal results refresh the RAM snapshot once while retaining the outcome message. No automatic POST retry. Result replacement stops automatic following; same-ID action mismatch, invalid HTTP status/schema or impossible loaded-defaults action fails validation. Lost acknowledgement, idle/replaced results and failures retain an explicit uncertainty warning with manual Check Result/Refresh recovery.
|
||
|
||
Domain/view navigation, pagehide, logout, expiry, identity replacement and late request/body completion are fenced by existing work generation plus Display request ownership/abort state. Drafts clear on teardown. Returning does not resume polling or replay a mutation. Backend work is not cancelled by browser navigation or timeout. Session replacement still requires a clean document rather than exposing retained terminal state to a new identity.
|
||
|
||
## Validation executed
|
||
|
||
All commands below ran successfully unless explicitly marked unavailable. Host storage/RTOS/HTTPD/renderer doubles are not target timing, power-loss or real-device evidence.
|
||
|
||
| Command | Result / scope |
|
||
|---|---|
|
||
| `python3 tests/web_cookie_auth/run.py --display` | PASS: **7 Display groups**, plus shared cookie/store suite. Compiles production web handler, public owner function bodies, unchanged full NVS config implementation, canonical CLI handlers, cookie/store/parser and installed-SDK boundary doubles. Schema/bounds, admin/auth, absent UI/busy projection, generation conflicts, CLI parity, real loader reboot projection, activity/diagnostic-hold interleaving, init/open/read/set/commit fault injection, replay isolation, session expiry/revocation/restart and lost ACK. |
|
||
| `python3 tests/admin_console_boundary/run.py` | PASS, including added Display zero-ID/not-ready/full-queue/FIFO routing with Serial/Accounts/Network in the unchanged four-entry queue. Existing certificate and SSH owner adapter suites pass. |
|
||
| `python3 tests/admin_console_boundary/lifecycle.py` | PASS canonical lifecycle/force/deferred owner isolation. |
|
||
| `python3 tests/admin_console_boundary/accounts.py` | PASS canonical accounts/keys/storage/currentness regressions. |
|
||
| `python3 tests/admin_ssh_policy/run.py` | PASS parsed policy, startup gates and completion regressions. |
|
||
| `python3 tests/web_admin_transport/server_lifecycle.py` | PASS **23 groups**, including all six Display staged descriptor/name failure positions, failed unregister, stop failure/restart, and other-domain failure isolation. |
|
||
| `python3 tests/web_ui_session/run.py` | PASS **111 browser groups**: prior 100 plus 11 Display groups; production C renderer, all resource/header failures, HTML structure/style and exact loader/CSP checks. All four views covered by structural fixtures. |
|
||
| `python3 tests/web_cookie_auth/run.py --settings` | PASS Settings/status/optional adapter and shared auth. |
|
||
| `python3 tests/web_cookie_auth/run.py --serial-settings` | PASS Serial mutation/CLI/failure/currentness regressions and shared auth. |
|
||
| `python3 tests/web_cookie_auth/run.py --accounts` | PASS account/key/credential policies and shared auth. |
|
||
| `python3 tests/web_cookie_auth/run.py --network` | PASS Network bounds/secret handling/owner admission and shared auth. |
|
||
| `python3 tests/web_cookie_auth/run.py --admin` | PASS real cookie/ticket/admin admission integration and shared auth. |
|
||
| `python3 tests/web_network_settings/run.py` | PASS 5 canonical Wi-Fi/mDNS owner groups. |
|
||
| `python3 tests/web_auth_parse/run.py` | PASS **294 cases, 0 failures**. |
|
||
| `python3 tests/web_admin_transport/run.py --tickets` | PASS transport and ticket suites. |
|
||
| `python3 tests/web_httpd_idle/run.py` | PASS idle lifecycle and SDK contract guards. |
|
||
| `python3 tests/web_diagnostics/run.py` | PASS diagnostics and SDK guards. |
|
||
| `python3 tests/web_session_store/run.py --serial` | PASS store and serial integration. |
|
||
| `python3 tests/web_serial_performance/run.py` | PASS preserved combined-send/failure isolation, installed-SDK wire contract and performance/epoch regressions. |
|
||
| `python3 tests/session_broker_diagnostics/run.py` | PASS **7 groups**, including bounded fanout/counter/ownership regressions. |
|
||
| `python3 tests/web_login_ui/run.py` | PASS renderer/CSP and **8 browser groups**. |
|
||
| `git --no-pager diff --check` | PASS. |
|
||
|
||
Optional real layout attempt: `WEB_UI_CHROMIUM=/usr/bin/chromium-browser python3 tests/web_ui_session/run.py` could not execute Chromium in the terminal sandbox: read-only crash-report storage and forbidden process-singleton `socket()`/ptrace. **No Chromium geometry/visual pass claimed.** Structural four-view fixtures pass; optional geometry fixtures were updated to all four views at 320/600/1200px (12 cases), but await an environment that permits Chromium. No dependency installation or broad sandbox escape was used. No sanitizer validation claimed.
|
||
|
||
Separate self-review checked owner reservation/public call sites, generation/publication/failure ordering, ID/session/deadline fences, HTTP route wiring, browser lifecycle/result validation and fixture coverage. Fixed result HTTP-status/action/replacement checking and expanded storage failures during that pass. This initial self-review was followed by an independent review of the final implementation: no actionable findings. Reviewer independently reran Display7+shared auth, dispatcher boundary, lifecycle23, UI111+C/HTML/CSP and diff checks, all PASS. Parent final `pio run` confirmation PASS9.08s at100,196 B RAM/1,765,233 B flash; diff check PASS. No target validation inferred.
|
||
|
||
## Build and resources
|
||
|
||
Actual pre-edit `pio run`: **PASS 12.10 s**, **100,100 B RAM / 1,748,513 B flash**. This matches the recorded combined-send size but was measured afresh in this worktree at CPU160MHz.
|
||
|
||
Final production `pio run`: **PASS 26.09 s**, **100,196 B RAM / 1,765,233 B flash**. Delta **+96 B RAM / +16,720 B flash** versus actual pre-edit baseline. Intermediate full configuration build passed in 71.95 s and emitted the existing nonfatal SDK `FATFS_PRINT_FLOAT` bool-default warning; final incremental build passed without that warning. No configuration was changed to suppress it.
|
||
|
||
Tracked defaults, active sdkconfig and generated `sdkconfig.h` explicitly select **160MHz**. PlatformIO's generic board banner prints 240MHz; it is not the compiled CPU selection.
|
||
|
||
Target toolchain object inspection (not host `sizeof`): Display slot **88 B**, operation ID **4 B**, portMUX **8 B**; UI config busy flag **1 B**, generation **4 B**. These are individual symbols before placement and do not by themselves explain the whole-image RAM delta. Three URI descriptors are **24 B each**. Successful startup requests **170 B additional route/table/name heap before allocator overhead**: 72 descriptor + 12 table-pointer + 86 NUL-terminated URI-name bytes. No new timer/task/mutex allocation.
|
||
|
||
Local Xtensa entry frames, excluding callees: operation HTTP handler **656 B**, snapshot HTTP handler **320 B**, dispatcher execute **128 B**, owner update **48 B**, owner snapshot **32 B**. These are not whole-call-chain requirements or runtime high-water margins. Rendered authored HTML/app/loader sizes: **26,973 / 97,829 / 1,173 bytes**. Browser allocations, runtime route heap overhead, HTTPD/dispatcher stack floors and loaded internal/DMA/PSRAM reserves remain unmeasured. Previously low lifetime memory minima remain follow-ups, not approved reserves or reopened prior functional acceptance.
|
||
|
||
## Pending target checklist — no execution/sign-off claimed
|
||
|
||
- [ ] Compare browser values with UART0/admin SSH `display status`, including 0/0, disabled Dim, disabled Off, 86,400 boundary and enabled ordering; reject malformed/direct API values without state changes.
|
||
- [ ] Verify actual dim/off/wake behavior and fresh activity after Apply/Load/Defaults/Reset; Save must not count as a wake/activity change.
|
||
- [ ] Change RAM without Save, reboot through an authorized independent control and confirm old saved values; explicitly Save, reboot and confirm new values. Confirm Defaults RAM-only and Reset persistence. No factory erase required.
|
||
- [ ] Missing/incompatible storage selects defaults on Load without silently replacing NVS; storage faults leave RAM unchanged. Test faults only through an approved safe test setup.
|
||
- [ ] With panel absent at boot but working buttons/UI task, read/apply/save/load configuration and observe continued UART0/native USB recovery. Reconnect panel safely and verify existing consumed-press reprobe/wake. Distinguish absent panel from failed I2C bus or unavailable UI task.
|
||
- [ ] While browser snapshot/operation is pending, edit through UART0/admin SSH and exercise buttons. Verify stale generation conflict, explicit Refresh/review, no overwritten CLI edit, no stuck reservation, no accidental button action or I2C ownership change.
|
||
- [ ] Two admin tabs/logins: global pending capacity, same-login result replacement warning, other-login result isolation, expired/revoked cookie denial, lost acknowledgement recovery, blocked dispatcher/dequeue expiry and HTTPS stop/restart fencing. Never interpret timeout as cancellation.
|
||
- [ ] Desktop/mobile narrow layout, keyboard focus, Refresh draft replacement, Reset confirmation, automatic completion bounds, manual uncertain-result recovery, logout/expiry/pagehide and same-session restoration.
|
||
- [ ] Repeat settled full mix at CPU160MHz/230400 baud with two browser serial clients, SSH/USB and both admins while using Display. Capture broker drops/HWM, service errors, free/minimum/largest internal/DMA/PSRAM and HTTPD/dispatcher stack high-water; distinguish acceptance from numeric reserve approval.
|
||
- [x] Independent diff/test review complete; no actionable findings.
|
||
- [ ] Explicit user target acceptance before claiming 8D.14 sign-off. No later-phase work is implied.
|