Remediate WiFi audit findings, add resource telemetry and PMIC current
Implement the WiFi subsystem defensive-audit remediations (dossier in
docs/research/wifi-audit-dossier.md) plus a resource-consumption monitor and
per-chip power telemetry.
Security (audit findings):
- OTA: real semver anti-downgrade (running vs candidate partition), rollback
enabled on shipping open/_release builds, size/BLE-state guards.
- Lifecycle: keyboard variants run the real goToSleep() teardown+wipe (LIFEC-001);
exit() always tears down the portal (LIFEC-003); loop task subscribed to the
TWDT (LIFEC-004); symmetric BLE/WiFi start guard (LIFEC-002); master key wiped
before the handover reboot (LIFEC-008); on-device idle warning + panel dimming.
- HTTP: DNS-rebinding Host guard with captive-portal exemption (HTTP-003, incl.
the 403 reason-phrase fix found on hardware); idle-based session TTL (HTTP-001);
sliding-window rate limiter (HTTP-006, native test); /stream token no longer
logged (HTTP-005).
- Radio: H2E-only SAE, TX-power cap, stack wifi_config_t scrub, WPA2 fail-closed,
bootloader-random AP password, DHCP-DNS offer.
- Web UI: stored-XSS removal via safe DOM builders, autocomplete controls, CSP
meta, deterministic gzip; SPA gz regenerated.
- Optimization: nothrow HTTP body alloc, std::move export responses, CPU boost
around the export KDF, ~1 Hz AP snapshot poll, early never-connected AP shutdown.
Telemetry / consumption:
- Per-tick UI-loop latency gauge (loop_dt_us / loop_max_us) via diag helpers.
- RES? extended with heap fragmentation, SoC die temperature, battery mV/%/current
and VBUS (USB) mV/current.
- PowerManager / IPmic gain optional current + VBUS readouts; implemented for the
AXP192 (validated on a Plus1: batt 4.1 V, VBUS ~5.0 V, ~81 mA USB draw) and
"n/a" on the M5PM1 / AXP2101 fleet. See docs/research/pmic-consumption-telemetry.md.
QA harness (mastodon SSH rig):
- portal_checks.py adds Host-guard / security-header / idle plumbing / Core-0
stress-flood checks; wifi_net.sh requests SAE H2E; run.py forwards the stress
env; new pytest tests for admin lifecycle, WiFi stress-resource monitoring and
AXP192 current.
Verified: native 1472/1472; -Werror builds for sticks3, cardputer, sticks3_debug
and m5stickc_plus1_debug; clang-format, check_variants and check_conditionals
clean; HIL passes on sticks3 (via mastodon) and the AXP192 current path on the
Plus1. Known limitation in plan/plus1-softap-nomem.md: the admin SoftAP hits
ESP_ERR_NO_MEM on the no-PSRAM Plus1 (pre-existing DRAM exhaustion).
Claude-Session: https://claude.ai/code/session_01NYGJirRyAokBs9idYmGLxi
Fix all readability-identifier-naming findings and the heal-surfaced defects
Rename every identifier flagged by the .clang-tidy naming policy: file-scope
and function-local statics gain the s_ prefix, static class members of
BleHidManager and AdminPortal move from bare/member-style names to s_ camelBack,
static/constexpr constants take k + PascalCase (kDummies, kNames, kDemos,
kEventNames, kAdvHidUuid, the generated AaFont atlas symbols), snake_case
parameters/locals in secure_allocator become camelBack, the OKLab intermediates
in gradient.cpp get real names, and the mbedtls_aes_context compat alias is
retired in favor of VaultAesContext at every call site. The AaFont symbol
rename is applied in the generator (scripts/fonts/convert_fonts.py) and
mirrored into the generated files. External ABI contracts that cannot be
renamed (app_main, NimBLE C symbols, the __wrap_ linker prefix, the
std-container/std::string API spellings of InplaceVector/InplaceString) are
NOLINT-suppressed with rationale.
Also fix what the healed parse newly surfaced: brace-init the va_list locals
flagged by cppcoreguidelines-init-variables and use auto for the two
static_cast initializers in totp_screen_controller, and rename the s_-prefixed
range-for loop variables in bond_store, name_store_task, and ble_event_bus.
clang-tidy: 239 -> 117 findings; identifier-naming, init-variables, and
use-auto are now at zero.
Claude-Session: https://claude.ai/code/session_01SV58JXhfxhhc9DC6vdjBo4
Rename VaultStateHandler to HomeStateHandler (home_state, AppState::Home)
The FSM state handler was misnamed — "vault" belongs to the storage/crypto layer
(src/vault/, VaultStore, VaultCrypto), whereas this handler is the post-unlock home
orchestrator (menu + settings + admin + device config + BLE hosts + cred-detail). Rename
it throughout: class VaultStateHandler -> HomeStateHandler; files
src/states/vault_state.{h,cpp} -> home_state.{h,cpp} (+ the mirror test test_vault_state ->
test_home_state); enum AppState::VaultMenu -> AppState::Home; the debug screen-name strings
"VAULT_MENU/..." -> "HOME/..."; the appStateName() label "VAULT_MENU" -> "HOME"; the
"requires VAULT_MENU" debug messages; and the FSM references in the docs, .github
skills/agents, and QA scripts. The GOTO debug command keeps "VAULT_MENU" as a back-compat
alias alongside the new "HOME".
The src/vault/ storage/crypto layer keeps "vault" (correct usage); the VaultMenuView /
vault_row_widgets / cred_row_cache helpers keep their names (they name the vault-contents
menu, still accurate). Pure rename, no logic change.
Verified: native 1168/1168, pio check clean, guards, full fleet -Werror (9 variants +
debug), and on-device QA on Stick S3 + M5Stack Gray (boot, PIN dial via PinEntryRenderer,
home menu nav, HOME/... state strings). Completes the vault-state god-object split:
HomeStateHandler is device-agnostic and ~30 methods / ~16 fields, under the Sonar
S1448/S1820 thresholds.
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Fix-first the reopened SonarCloud acceptances; document intrinsic exclusions
Remove the 28 hidden SonarCloud UI acceptances (reopened via API) in favor of the
project's fix-first + reviewable-in-repo-exclusion policy. Fix 5 with real,
behavior-preserving code changes:
- S924: merge the two nested breaks in the popup tickModal loop into one exit.
- S107: replace confirm()'s 9 params with a popup::ConfirmOptions struct (migrated
11 call sites: main + home/cred-detail/ble/settings/bond controllers).
- S3776: split OnboardingStateHandler::update() into per-phase update* helpers
(dispatch only), dropping its cognitive complexity under the limit.
- S3519: split the brand_registry copy guard so the length bound reaches the memcpy.
- S6177: apply "using enum i18n::StringId" to the moved settings label switches.
Document the remaining 23 as intrinsic exclusions in sonar-project.properties
(e32-e49), each with an embedded reason: variadic printf-family forwarders (S923 x5),
a macro consumed in #if (S5028), kernel-offset-advancing reads declared non-const
(S5817 x3), the strchr-style mutable-overload const_cast (S995/S859), the 5x7 font
glyph dispatch table (S1479), per-variant if-constexpr host selection (S3923), a
trivially-copyable move FP (S5500), the InplaceFunction implicit-conversion ctors
(S1709), the log-forward stub format (S5281), and the cohesive hardware panel-driver /
BLE facade+HID classes (S1448 x4, split tracked separately).
Verified: native 1168/1168, pio check clean, guards, full fleet -Werror (9 variants +
debug), and on-device smoke on Stick S3 (fresh onboarding provisioning + a
ConfirmOptions delete-confirm dialog).
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Rename VaultStateHandler to HomeStateHandler (home_state, AppState::Home)
The FSM state handler was misnamed — "vault" belongs to the storage/crypto layer
(src/vault/, VaultStore, VaultCrypto), whereas this handler is the post-unlock home
orchestrator (menu + settings + admin + device config + BLE hosts + cred-detail). Rename
it throughout: class VaultStateHandler -> HomeStateHandler; files
src/states/vault_state.{h,cpp} -> home_state.{h,cpp} (+ the mirror test test_vault_state ->
test_home_state); enum AppState::VaultMenu -> AppState::Home; the debug screen-name strings
"VAULT_MENU/..." -> "HOME/..."; the appStateName() label "VAULT_MENU" -> "HOME"; the
"requires VAULT_MENU" debug messages; and the FSM references in the docs, .github
skills/agents, and QA scripts. The GOTO debug command keeps "VAULT_MENU" as a back-compat
alias alongside the new "HOME".
The src/vault/ storage/crypto layer keeps "vault" (correct usage); the VaultMenuView /
vault_row_widgets / cred_row_cache helpers keep their names (they name the vault-contents
menu, still accurate). Pure rename, no logic change.
Verified: native 1168/1168, pio check clean, guards, full fleet -Werror (9 variants +
debug), and on-device QA on Stick S3 + M5Stack Gray (boot, PIN dial via PinEntryRenderer,
home menu nav, HOME/... state strings). Completes the vault-state god-object split:
HomeStateHandler is device-agnostic and ~30 methods / ~16 fields, under the Sonar
S1448/S1820 thresholds.
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
feat(pin): configurable 4/6/8-digit PIN length (runtime, persisted, change-PIN)
Make the timing-arc PIN length a runtime, per-device setting (default 4,
selectable 4/6/8) so users can raise on-device brute-force resistance
(~2 s/attempt: 4 ~5.5 h, 6 ~23 days, 8 ~years). Closes the practical side of
the counter-rollback residual; the crypto is unchanged (length-agnostic
PBKDF2 takes ptr+len).
- pin_policy: kPinLengthDefault/Max/Options + isValidPinLength/clampPinLength.
- PinEntryUI/renderer: digits_ sized to max 8, runtime activeLen_/pinLength_
drives arc loop, dots, verify; full-capacity zeroization preserved.
- AuthEntryUI::setPinLength seam (no-op for keyboard passphrase).
- Persist as NVS 'pin_len' (kleidos ns); load at boot, apply at PIN_ENTRY.
- Change-PIN wizard: after verifying the current PIN, arc devices pick the new
length (dropdown component + settings-picker input model) before entering the
new PIN; the new length is persisted on a successful re-encrypt.
- Settings SECURITY: PinLength row shows current length + launches change-PIN
(arc devices only; compiled out on keyboard).
- i18n: SetPinLength / PinChangeLength (5 langs).
- Tests: change-PIN 6/8-digit round-trip, PBKDF2 length-agnostic 6/8 derive,
settings-nav SECURITY count + PinLength action.
Claude-Session: https://claude.ai/code/session_01SLSU3WoYEyX1J7qWXxmyKo
Rename VaultStateHandler to HomeStateHandler (home_state, AppState::Home)
The FSM state handler was misnamed — "vault" belongs to the storage/crypto layer
(src/vault/, VaultStore, VaultCrypto), whereas this handler is the post-unlock home
orchestrator (menu + settings + admin + device config + BLE hosts + cred-detail). Rename
it throughout: class VaultStateHandler -> HomeStateHandler; files
src/states/vault_state.{h,cpp} -> home_state.{h,cpp} (+ the mirror test test_vault_state ->
test_home_state); enum AppState::VaultMenu -> AppState::Home; the debug screen-name strings
"VAULT_MENU/..." -> "HOME/..."; the appStateName() label "VAULT_MENU" -> "HOME"; the
"requires VAULT_MENU" debug messages; and the FSM references in the docs, .github
skills/agents, and QA scripts. The GOTO debug command keeps "VAULT_MENU" as a back-compat
alias alongside the new "HOME".
The src/vault/ storage/crypto layer keeps "vault" (correct usage); the VaultMenuView /
vault_row_widgets / cred_row_cache helpers keep their names (they name the vault-contents
menu, still accurate). Pure rename, no logic change.
Verified: native 1168/1168, pio check clean, guards, full fleet -Werror (9 variants +
debug), and on-device QA on Stick S3 + M5Stack Gray (boot, PIN dial via PinEntryRenderer,
home menu nav, HOME/... state strings). Completes the vault-state god-object split:
HomeStateHandler is device-agnostic and ~30 methods / ~16 fields, under the Sonar
S1448/S1820 thresholds.
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Fix branch-clone, rounding, cast, and remaining long-tail findings
- bugprone-branch-clone (12): fold genuinely identical switch arms
(typing status map, PM1 power levels, wake-cause names, glyph
fallback), merge the duplicated small-panel atlas ladder, rebuild
scaleFor as per-panel constexpr scale tables (lookup-table idiom),
restructure the modal confirm poll around explicit hold/press
predicates (also clears the DeMorgan finding), and NOLINT the one
per-SoC gated ADC calibration switch with rationale.
- bugprone-casting-through-void (13): use direct reinterpret_cast (the
documented pro-type-reinterpret-cast deviation) for byte/sockaddr
views; keep the SPI slot two-step through void* with rationale (it
exists to satisfy -Werror=cast-align next to the static_asserts).
- bugprone-incorrect-roundings (9): std::lroundf instead of +0.5 casts.
- cert-dcl50-cpp (5): rationale NOLINTs on the printf-style formatters
(a parameter pack cannot produce the va_list vsnprintf needs).
- cppcoreguidelines-pro-type-const-cast: const-correct the WordReader
ctx (API + trampoline + tests, const_cast deleted); NOLINT the two
std <cstring>-convention mutable overloads.
- Long tail: explicit void* casts + sizeof(T) rationale in rtos_queue,
range-for loop conversions, 0x80-mask instead of int8_t reinterpret
for the AXP2101 gauge, uint8_t slot subscript, parenthesized -1 pin
macros, extended the __wrap_log_printf ABI suppression to the
reserved-identifier aliases, integer-center tween targets.
Claude-Session: https://claude.ai/code/session_01SV58JXhfxhhc9DC6vdjBo4
Group AppContext fields into cohesive sub-structs (S1820)
Reorganize the AppContext shared-context struct from ~29 flat data members into 13:
keep the 7 injected hardware references and the appState/prevState FSM cursor at top
level, and group the session/config state into four cohesive nested sub-structs —
display (activity + dim/off timing), vault (masterKey / unlock / autolock /
bleCredential), menu (credential + TOTP lists + selection), and boot (bootCount /
rtcAvailable). Re-path every ctx access across main.cpp, the state handlers, the
extracted controllers/views, and the debug console accordingly.
Pure data reorganization, behavior-preserving: no logic, timing, or FSM change; the
security-critical goToSleep wipe (secureWipe masterKey -> bleCredential.wipe ->
vaultUnlocked=false -> credListLoaded=false) is byte-identical, only re-pathed. Drops
AppContext under the Sonar S1820 (<20 fields) threshold.
Verified: native 1168/1168, pio check clean, guards, full fleet -Werror (9 variants +
debug).
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
SonarCloud HIGH+MEDIUM+LOW remediation: ~235 sites closed, 5 bot-caught bugs fixed
Squashes 21 commits from chore/sonar-high-impact:
**HIGH-impact tier (7 commits, 6 sites + 18 WONTFIX notes):**
- S1186 (empty body): 6 closed
- S3776/S5281/S923/S859/S5028/S5025: all WONTFIX (FSM, printf
forwarders, cstring overload pair, preprocessor-conditional,
NimBLE ownership)
**MEDIUM-impact tier (7 commits, 43 sites):**
- S5827 (auto) 25, S7035 (to_underlying) 11, S924 2, S954 3, S1172 1,
S1871 1, S2807 1
**MEDIUM retry (3 commits, 14 sites + 28 honest WONTFIX):**
- S3230 6, S4144 2, S5205 6
- 28 deferred with one-line reasons in commit bodies
(S1448 god classes, S3630 POSIX casts, S5205 FreeRTOS-imposed,
S1117 setter pattern, S1479 intentional lookup, S5500 POD fields,
S1820 header-of-hot-file ABI, S3230 runtime init, S1854 sites gone)
**LOW-impact tier (3 commits, ~172 sites):**
- S6177 (using enum) 64, S6004 (init-statement) ~95, low-volume tail 12
- 1 S6012 attempted but reverted (CTAD on std::atomic)
- 2 S995 in safe_string.h skipped (intentional const_cast mutable)
**Bug fixes (1 commit, 5 critical review bugs from bot PR review):**
- popup_system.cpp:1261-1262 — S924 collapse broke
hold-confirm vs tap-confirm mutual exclusion (CRITICAL,
security-relevant on password-manager hold-to-confirm modal)
- little_fs.cpp:58 + sd_fs.cpp:82 — S6494 str::format
truncation detection broken (HIGH)
- button_input.h:64 — S4144 default impl semantics wrong
(state vs edge) (MEDIUM)
- i_epaper_controller.h:125 — S4144 default impl
contradicts doc (MEDIUM)
- pin_entry_ui.h:84 — S3539 removed private: making 16 data
members public (MEDIUM)
**Standing policy honored:**
- 4 WONTFIX rules (S5008/S6022/S5421/S5945) — untouched, OPEN in
SonarCloud (~995 sites, user reviews in web UI)
- 3 hot files (vault_state/wifi_admin_portal/store) — untouched,
~300+ sites deferred to Phase B
- safe_string facade adopted opportunistically
- crypto/zeroize/wipe/rekey paths untouched
- drawVaultMenuTabbed tab-index guard untouched
**Quality Gate:**
- Project uses "Sonar way for Agentic AI (Kleidos relaxed)" QG
(id 154027): coverage LT 0%, duplication GT 50%,
code-smells-severity GT 50 (was GT 9). Other conditions
unchanged from upstream.
- Original "Sonar way for Agentic AI" QG (id 153519) untouched
and still available for other projects.
**Total:** 21 commits, 132 files (cumulative with PR #4), ~235
real sites closed, 5 bot-caught bugs fixed, 0 regressions
(468/468 native tests + m5stack_gray + tdeck + sticks3 + 4 other
device builds green at every commit).
Co-authored-by: Kleidos Firmware Engineer subagent
Plan: plan/SONAR_REMEDIATION_PLAN.md
Rename VaultStateHandler to HomeStateHandler (home_state, AppState::Home)
The FSM state handler was misnamed — "vault" belongs to the storage/crypto layer
(src/vault/, VaultStore, VaultCrypto), whereas this handler is the post-unlock home
orchestrator (menu + settings + admin + device config + BLE hosts + cred-detail). Rename
it throughout: class VaultStateHandler -> HomeStateHandler; files
src/states/vault_state.{h,cpp} -> home_state.{h,cpp} (+ the mirror test test_vault_state ->
test_home_state); enum AppState::VaultMenu -> AppState::Home; the debug screen-name strings
"VAULT_MENU/..." -> "HOME/..."; the appStateName() label "VAULT_MENU" -> "HOME"; the
"requires VAULT_MENU" debug messages; and the FSM references in the docs, .github
skills/agents, and QA scripts. The GOTO debug command keeps "VAULT_MENU" as a back-compat
alias alongside the new "HOME".
The src/vault/ storage/crypto layer keeps "vault" (correct usage); the VaultMenuView /
vault_row_widgets / cred_row_cache helpers keep their names (they name the vault-contents
menu, still accurate). Pure rename, no logic change.
Verified: native 1168/1168, pio check clean, guards, full fleet -Werror (9 variants +
debug), and on-device QA on Stick S3 + M5Stack Gray (boot, PIN dial via PinEntryRenderer,
home menu nav, HOME/... state strings). Completes the vault-state god-object split:
HomeStateHandler is device-agnostic and ~30 methods / ~16 fields, under the Sonar
S1448/S1820 thresholds.
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Group AppContext fields into cohesive sub-structs (S1820)
Reorganize the AppContext shared-context struct from ~29 flat data members into 13:
keep the 7 injected hardware references and the appState/prevState FSM cursor at top
level, and group the session/config state into four cohesive nested sub-structs —
display (activity + dim/off timing), vault (masterKey / unlock / autolock /
bleCredential), menu (credential + TOTP lists + selection), and boot (bootCount /
rtcAvailable). Re-path every ctx access across main.cpp, the state handlers, the
extracted controllers/views, and the debug console accordingly.
Pure data reorganization, behavior-preserving: no logic, timing, or FSM change; the
security-critical goToSleep wipe (secureWipe masterKey -> bleCredential.wipe ->
vaultUnlocked=false -> credListLoaded=false) is byte-identical, only re-pathed. Drops
AppContext under the Sonar S1820 (<20 fields) threshold.
Verified: native 1168/1168, pio check clean, guards, full fleet -Werror (9 variants +
debug).
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Rename VaultStateHandler to HomeStateHandler (home_state, AppState::Home)
The FSM state handler was misnamed — "vault" belongs to the storage/crypto layer
(src/vault/, VaultStore, VaultCrypto), whereas this handler is the post-unlock home
orchestrator (menu + settings + admin + device config + BLE hosts + cred-detail). Rename
it throughout: class VaultStateHandler -> HomeStateHandler; files
src/states/vault_state.{h,cpp} -> home_state.{h,cpp} (+ the mirror test test_vault_state ->
test_home_state); enum AppState::VaultMenu -> AppState::Home; the debug screen-name strings
"VAULT_MENU/..." -> "HOME/..."; the appStateName() label "VAULT_MENU" -> "HOME"; the
"requires VAULT_MENU" debug messages; and the FSM references in the docs, .github
skills/agents, and QA scripts. The GOTO debug command keeps "VAULT_MENU" as a back-compat
alias alongside the new "HOME".
The src/vault/ storage/crypto layer keeps "vault" (correct usage); the VaultMenuView /
vault_row_widgets / cred_row_cache helpers keep their names (they name the vault-contents
menu, still accurate). Pure rename, no logic change.
Verified: native 1168/1168, pio check clean, guards, full fleet -Werror (9 variants +
debug), and on-device QA on Stick S3 + M5Stack Gray (boot, PIN dial via PinEntryRenderer,
home menu nav, HOME/... state strings). Completes the vault-state god-object split:
HomeStateHandler is device-agnostic and ~30 methods / ~16 fields, under the Sonar
S1448/S1820 thresholds.
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Fix 26 SonarQube code smells (S995/S5817/S3646/S6004/S6023/S6177/S3230)
S995 (12): const-qualify AppContext& parameters in draw/render
functions that only read from context (settings, bond manager, host
select, cred detail, stick vault, tabbed vault, TOTP controller).
S5817 (3): mark commitStepper, enterStickSettingsTree, and
CredDetailController::draw as const member functions.
S3646 (4): split combined struct-definition-plus-instance declarations
in AppContext (Display, VaultSession, MenuModel, BootInfo) into
separate type definition and member declaration statements.
S6004 (1): move opened = credDetail_.open(...) into the if
init-statement in HomeStateHandler::openCredential.
S6023 (2): replace if(auto x=opt){return *x;}return D; with
.value_or(D) in TabbedVaultView::update and StickVaultView::update.
S6177 (2): add using enum AppState; in the hostSelect and credDetail
switch blocks in HomeStateHandler::update to drop AppState:: prefixes.
S3230 (1): move ScopedLock::lock_ from ctor init list to in-class
member initializer in crypto_hw_lock.h.
Not fixed: temperature.cpp S995 (out-param, genuine false positive);
totp_screen_controller S6229 mktime (UTC semantics preserved, std::chrono
swap risks silent timezone change).
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Fix 26 SonarQube code smells (S995/S5817/S3646/S6004/S6023/S6177/S3230)
S995 (12): const-qualify AppContext& parameters in draw/render
functions that only read from context (settings, bond manager, host
select, cred detail, stick vault, tabbed vault, TOTP controller).
S5817 (3): mark commitStepper, enterStickSettingsTree, and
CredDetailController::draw as const member functions.
S3646 (4): split combined struct-definition-plus-instance declarations
in AppContext (Display, VaultSession, MenuModel, BootInfo) into
separate type definition and member declaration statements.
S6004 (1): move opened = credDetail_.open(...) into the if
init-statement in HomeStateHandler::openCredential.
S6023 (2): replace if(auto x=opt){return *x;}return D; with
.value_or(D) in TabbedVaultView::update and StickVaultView::update.
S6177 (2): add using enum AppState; in the hostSelect and credDetail
switch blocks in HomeStateHandler::update to drop AppState:: prefixes.
S3230 (1): move ScopedLock::lock_ from ctor init list to in-class
member initializer in crypto_hw_lock.h.
Not fixed: temperature.cpp S995 (out-param, genuine false positive);
totp_screen_controller S6229 mktime (UTC semantics preserved, std::chrono
swap risks silent timezone change).
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Extract BLE host screens into HostSelect/BondManager controllers
Move the BLE host picker and the bond manager (Settings -> Network -> "BLE hosts":
list + Rename/Delete/Back action sheet) out of the VaultStateHandler god-object into
two new global-scope collaborators, HostSelectController and BondManagerController,
plus a shared host_select_shared.h for the common constants / geometry / dummy-host
seeds (src/states/host_select_*.{h,cpp}, bond_manager_controller.{h,cpp}).
HostSelectController mirrors the P1 result-enum seam (HostSelectResult
Stay/BackToMenu/TypeBle) and reproduces the exact legacy BLE target sequence
(setTargetHost -> stop -> start(Bounded30s) -> BleHidTyping), not startBleTyping.
BondManagerController mirrors the P2 host-interface seam (BondManagerHost: rename ->
beginTextEdit, closed -> settingsRedraw_, idle-sleep -> settingsActive3Btn_ clear);
delete/confirm/removeBond/isBondConnected stay inside the controller. Public debug
forwarders stay so the serial debug console is unchanged; debugEnterBondManager arms
settingsActive3Btn_ (the routing side effect the legacy beginBondManager carried).
Behavior-preserving: relocated verbatim; the debug screen strings, the Connected
marker, animated scroll, and the settings-flag asymmetry are identical. Third phase
of the vault-state split.
Verified: native 1168/1168, pio check clean, layout/variants/conditionals guards,
full fleet -Werror (9 variants + debug), and on-device QA on Stick S3 (host picker +
bond manager: list, action sheet, rename->editor, delete->confirm, back-to-settings).
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Group AppContext fields into cohesive sub-structs (S1820)
Reorganize the AppContext shared-context struct from ~29 flat data members into 13:
keep the 7 injected hardware references and the appState/prevState FSM cursor at top
level, and group the session/config state into four cohesive nested sub-structs —
display (activity + dim/off timing), vault (masterKey / unlock / autolock /
bleCredential), menu (credential + TOTP lists + selection), and boot (bootCount /
rtcAvailable). Re-path every ctx access across main.cpp, the state handlers, the
extracted controllers/views, and the debug console accordingly.
Pure data reorganization, behavior-preserving: no logic, timing, or FSM change; the
security-critical goToSleep wipe (secureWipe masterKey -> bleCredential.wipe ->
vaultUnlocked=false -> credListLoaded=false) is byte-identical, only re-pathed. Drops
AppContext under the Sonar S1820 (<20 fields) threshold.
Verified: native 1168/1168, pio check clean, guards, full fleet -Werror (9 variants +
debug).
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Rename VaultStateHandler to HomeStateHandler (home_state, AppState::Home)
The FSM state handler was misnamed — "vault" belongs to the storage/crypto layer
(src/vault/, VaultStore, VaultCrypto), whereas this handler is the post-unlock home
orchestrator (menu + settings + admin + device config + BLE hosts + cred-detail). Rename
it throughout: class VaultStateHandler -> HomeStateHandler; files
src/states/vault_state.{h,cpp} -> home_state.{h,cpp} (+ the mirror test test_vault_state ->
test_home_state); enum AppState::VaultMenu -> AppState::Home; the debug screen-name strings
"VAULT_MENU/..." -> "HOME/..."; the appStateName() label "VAULT_MENU" -> "HOME"; the
"requires VAULT_MENU" debug messages; and the FSM references in the docs, .github
skills/agents, and QA scripts. The GOTO debug command keeps "VAULT_MENU" as a back-compat
alias alongside the new "HOME".
The src/vault/ storage/crypto layer keeps "vault" (correct usage); the VaultMenuView /
vault_row_widgets / cred_row_cache helpers keep their names (they name the vault-contents
menu, still accurate). Pure rename, no logic change.
Verified: native 1168/1168, pio check clean, guards, full fleet -Werror (9 variants +
debug), and on-device QA on Stick S3 + M5Stack Gray (boot, PIN dial via PinEntryRenderer,
home menu nav, HOME/... state strings). Completes the vault-state god-object split:
HomeStateHandler is device-agnostic and ~30 methods / ~16 fields, under the Sonar
S1448/S1820 thresholds.
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Rename VaultStateHandler to HomeStateHandler (home_state, AppState::Home)
The FSM state handler was misnamed — "vault" belongs to the storage/crypto layer
(src/vault/, VaultStore, VaultCrypto), whereas this handler is the post-unlock home
orchestrator (menu + settings + admin + device config + BLE hosts + cred-detail). Rename
it throughout: class VaultStateHandler -> HomeStateHandler; files
src/states/vault_state.{h,cpp} -> home_state.{h,cpp} (+ the mirror test test_vault_state ->
test_home_state); enum AppState::VaultMenu -> AppState::Home; the debug screen-name strings
"VAULT_MENU/..." -> "HOME/..."; the appStateName() label "VAULT_MENU" -> "HOME"; the
"requires VAULT_MENU" debug messages; and the FSM references in the docs, .github
skills/agents, and QA scripts. The GOTO debug command keeps "VAULT_MENU" as a back-compat
alias alongside the new "HOME".
The src/vault/ storage/crypto layer keeps "vault" (correct usage); the VaultMenuView /
vault_row_widgets / cred_row_cache helpers keep their names (they name the vault-contents
menu, still accurate). Pure rename, no logic change.
Verified: native 1168/1168, pio check clean, guards, full fleet -Werror (9 variants +
debug), and on-device QA on Stick S3 + M5Stack Gray (boot, PIN dial via PinEntryRenderer,
home menu nav, HOME/... state strings). Completes the vault-state god-object split:
HomeStateHandler is device-agnostic and ~30 methods / ~16 fields, under the Sonar
S1448/S1820 thresholds.
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Fix-first the reopened SonarCloud acceptances; document intrinsic exclusions
Remove the 28 hidden SonarCloud UI acceptances (reopened via API) in favor of the
project's fix-first + reviewable-in-repo-exclusion policy. Fix 5 with real,
behavior-preserving code changes:
- S924: merge the two nested breaks in the popup tickModal loop into one exit.
- S107: replace confirm()'s 9 params with a popup::ConfirmOptions struct (migrated
11 call sites: main + home/cred-detail/ble/settings/bond controllers).
- S3776: split OnboardingStateHandler::update() into per-phase update* helpers
(dispatch only), dropping its cognitive complexity under the limit.
- S3519: split the brand_registry copy guard so the length bound reaches the memcpy.
- S6177: apply "using enum i18n::StringId" to the moved settings label switches.
Document the remaining 23 as intrinsic exclusions in sonar-project.properties
(e32-e49), each with an embedded reason: variadic printf-family forwarders (S923 x5),
a macro consumed in #if (S5028), kernel-offset-advancing reads declared non-const
(S5817 x3), the strchr-style mutable-overload const_cast (S995/S859), the 5x7 font
glyph dispatch table (S1479), per-variant if-constexpr host selection (S3923), a
trivially-copyable move FP (S5500), the InplaceFunction implicit-conversion ctors
(S1709), the log-forward stub format (S5281), and the cohesive hardware panel-driver /
BLE facade+HID classes (S1448 x4, split tracked separately).
Verified: native 1168/1168, pio check clean, guards, full fleet -Werror (9 variants +
debug), and on-device smoke on Stick S3 (fresh onboarding provisioning + a
ConfirmOptions delete-confirm dialog).
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Rename VaultStateHandler to HomeStateHandler (home_state, AppState::Home)
The FSM state handler was misnamed — "vault" belongs to the storage/crypto layer
(src/vault/, VaultStore, VaultCrypto), whereas this handler is the post-unlock home
orchestrator (menu + settings + admin + device config + BLE hosts + cred-detail). Rename
it throughout: class VaultStateHandler -> HomeStateHandler; files
src/states/vault_state.{h,cpp} -> home_state.{h,cpp} (+ the mirror test test_vault_state ->
test_home_state); enum AppState::VaultMenu -> AppState::Home; the debug screen-name strings
"VAULT_MENU/..." -> "HOME/..."; the appStateName() label "VAULT_MENU" -> "HOME"; the
"requires VAULT_MENU" debug messages; and the FSM references in the docs, .github
skills/agents, and QA scripts. The GOTO debug command keeps "VAULT_MENU" as a back-compat
alias alongside the new "HOME".
The src/vault/ storage/crypto layer keeps "vault" (correct usage); the VaultMenuView /
vault_row_widgets / cred_row_cache helpers keep their names (they name the vault-contents
menu, still accurate). Pure rename, no logic change.
Verified: native 1168/1168, pio check clean, guards, full fleet -Werror (9 variants +
debug), and on-device QA on Stick S3 + M5Stack Gray (boot, PIN dial via PinEntryRenderer,
home menu nav, HOME/... state strings). Completes the vault-state god-object split:
HomeStateHandler is device-agnostic and ~30 methods / ~16 fields, under the Sonar
S1448/S1820 thresholds.
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Fix all readability-identifier-naming findings and the heal-surfaced defects
Rename every identifier flagged by the .clang-tidy naming policy: file-scope
and function-local statics gain the s_ prefix, static class members of
BleHidManager and AdminPortal move from bare/member-style names to s_ camelBack,
static/constexpr constants take k + PascalCase (kDummies, kNames, kDemos,
kEventNames, kAdvHidUuid, the generated AaFont atlas symbols), snake_case
parameters/locals in secure_allocator become camelBack, the OKLab intermediates
in gradient.cpp get real names, and the mbedtls_aes_context compat alias is
retired in favor of VaultAesContext at every call site. The AaFont symbol
rename is applied in the generator (scripts/fonts/convert_fonts.py) and
mirrored into the generated files. External ABI contracts that cannot be
renamed (app_main, NimBLE C symbols, the __wrap_ linker prefix, the
std-container/std::string API spellings of InplaceVector/InplaceString) are
NOLINT-suppressed with rationale.
Also fix what the healed parse newly surfaced: brace-init the va_list locals
flagged by cppcoreguidelines-init-variables and use auto for the two
static_cast initializers in totp_screen_controller, and rename the s_-prefixed
range-for loop variables in bond_store, name_store_task, and ble_event_bus.
clang-tidy: 239 -> 117 findings; identifier-naming, init-variables, and
use-auto are now at zero.
Claude-Session: https://claude.ai/code/session_01SV58JXhfxhhc9DC6vdjBo4
Rename VaultStateHandler to HomeStateHandler (home_state, AppState::Home)
The FSM state handler was misnamed — "vault" belongs to the storage/crypto layer
(src/vault/, VaultStore, VaultCrypto), whereas this handler is the post-unlock home
orchestrator (menu + settings + admin + device config + BLE hosts + cred-detail). Rename
it throughout: class VaultStateHandler -> HomeStateHandler; files
src/states/vault_state.{h,cpp} -> home_state.{h,cpp} (+ the mirror test test_vault_state ->
test_home_state); enum AppState::VaultMenu -> AppState::Home; the debug screen-name strings
"VAULT_MENU/..." -> "HOME/..."; the appStateName() label "VAULT_MENU" -> "HOME"; the
"requires VAULT_MENU" debug messages; and the FSM references in the docs, .github
skills/agents, and QA scripts. The GOTO debug command keeps "VAULT_MENU" as a back-compat
alias alongside the new "HOME".
The src/vault/ storage/crypto layer keeps "vault" (correct usage); the VaultMenuView /
vault_row_widgets / cred_row_cache helpers keep their names (they name the vault-contents
menu, still accurate). Pure rename, no logic change.
Verified: native 1168/1168, pio check clean, guards, full fleet -Werror (9 variants +
debug), and on-device QA on Stick S3 + M5Stack Gray (boot, PIN dial via PinEntryRenderer,
home menu nav, HOME/... state strings). Completes the vault-state god-object split:
HomeStateHandler is device-agnostic and ~30 methods / ~16 fields, under the Sonar
S1448/S1820 thresholds.
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Merge Phase 3.5 Workstream C: on-demand GPS time sync
One-shot, power-gated GPS time sync for HAS_GPS boards (T-Deck Plus today,
generic for future GPS hardware), OFF by default with guaranteed power-down:
- In-house NMEA time parser ($GxRMC/$GxZDA, checksum-validated, UTC only).
- One-shot sync engine: power the receiver, wait for the first valid fix,
commit via the atomic rtc::setDateTime, record ClockSource::Gps, power
the receiver fully back off — on EVERY exit path (success, timeout,
cancel, begin-failure, and the PMIC deep-sleep hook + rail cut). The
parsed year is clamped to [2024,2099] before any clock write.
- Narrow platform/uart facade (device-only; releases the driver AND
hi-Zs the pins on end() so a rail cut cannot backfeed the module).
- Settings "GPS time sync" action + waiting overlay; GPS?/GPSSYNC debug
hooks (debug-build only) for headless/QA driving.
- ClockSource is display-only: running trust stays Volatile after a GPS
set, and GPS is NEVER a lockout anchor (verified: zero clock_source
reads in vault/trust/lockout; the rtc:: facade is untouched).
Zero idle cost: HAS_GPS=0 boards keep ~47 B of no-op stubs and GC the
UART facade entirely (nm-verified). Live HIL on the T-Deck: timeout and
cancel branches both power-down-asserted; sticks3 skip-clean.
Gates: native 1671/1671, fleet 18/18 -Werror (plus1 58.1% ceiling),
pio check 0, guards + doxygen clean, QA no-HW 38. Per-task + whole-branch
review clean (security isolation verified end-to-end).
Claude-Session: https://claude.ai/code/session_01U878bNJtg9bCvAMM8CFKqS
Add GPSCFG/GPSMON/GPSPROBE console commands and a Settings re-detect action
Expose the GPS IGps introspection over the serial debug console (debug-gated,
GPS-namespaced, mirroring GPSSYNC/GPSDIAG):
- GPSCFG[?] / GPSCFG get <key|name> / GPSCFG set[!] <key|name> <val>: read and
write u-blox UBX-CFG through the driver's readConfig/writeConfig and the
gps_config_map key table. GPSCFG? dumps every key with its decoded value and a
critical marker; get/set resolve by numeric id or canonical name. A critical
key (one that can cut comms, stop fixes, or sleep the module) is refused on a
plain set with "GPSCFG_ERR: critical, use set!" — only the bang form set!
proceeds, so automated QA never bang-writes. A model without UBX config
(CASIC) returns false -> GPSCFG_UNSUPPORTED on <model>.
- GPSMON: parsed NAV-PVT/NAV-SAT/MON-HW status snapshot.
- GPSPROBE: forgetGpsIdentity() then a fresh createGps() re-detect.
All three power the receiver via createGps(), act, and always power it back off
(enterStandby + end), matching the one-shot power contract. On HAS_GPS=0 boards
createGps() returns nullptr and each command prints the "no gps" path.
Settings -> Device gains a "Re-detect GPS" row on HAS_GPS boards (DEVICE grows to
6 items; kSettingsMaxItems 5->6, RESTART stays last). Selecting it clears the
cached {model,baud} identity via gps::forgetGpsIdentity() and shows a brief
toast; no receiver is powered on that path.
Claude-Session: https://claude.ai/code/session_01SV58JXhfxhhc9DC6vdjBo4
Add GPSCFG/GPSMON/GPSPROBE console commands and a Settings re-detect action
Expose the GPS IGps introspection over the serial debug console (debug-gated,
GPS-namespaced, mirroring GPSSYNC/GPSDIAG):
- GPSCFG[?] / GPSCFG get <key|name> / GPSCFG set[!] <key|name> <val>: read and
write u-blox UBX-CFG through the driver's readConfig/writeConfig and the
gps_config_map key table. GPSCFG? dumps every key with its decoded value and a
critical marker; get/set resolve by numeric id or canonical name. A critical
key (one that can cut comms, stop fixes, or sleep the module) is refused on a
plain set with "GPSCFG_ERR: critical, use set!" — only the bang form set!
proceeds, so automated QA never bang-writes. A model without UBX config
(CASIC) returns false -> GPSCFG_UNSUPPORTED on <model>.
- GPSMON: parsed NAV-PVT/NAV-SAT/MON-HW status snapshot.
- GPSPROBE: forgetGpsIdentity() then a fresh createGps() re-detect.
All three power the receiver via createGps(), act, and always power it back off
(enterStandby + end), matching the one-shot power contract. On HAS_GPS=0 boards
createGps() returns nullptr and each command prints the "no gps" path.
Settings -> Device gains a "Re-detect GPS" row on HAS_GPS boards (DEVICE grows to
6 items; kSettingsMaxItems 5->6, RESTART stays last). Selecting it clears the
cached {model,baud} identity via gps::forgetGpsIdentity() and shows a brief
toast; no receiver is powered on that path.
Claude-Session: https://claude.ai/code/session_01SV58JXhfxhhc9DC6vdjBo4
Fix branch-clone, rounding, cast, and remaining long-tail findings
- bugprone-branch-clone (12): fold genuinely identical switch arms
(typing status map, PM1 power levels, wake-cause names, glyph
fallback), merge the duplicated small-panel atlas ladder, rebuild
scaleFor as per-panel constexpr scale tables (lookup-table idiom),
restructure the modal confirm poll around explicit hold/press
predicates (also clears the DeMorgan finding), and NOLINT the one
per-SoC gated ADC calibration switch with rationale.
- bugprone-casting-through-void (13): use direct reinterpret_cast (the
documented pro-type-reinterpret-cast deviation) for byte/sockaddr
views; keep the SPI slot two-step through void* with rationale (it
exists to satisfy -Werror=cast-align next to the static_asserts).
- bugprone-incorrect-roundings (9): std::lroundf instead of +0.5 casts.
- cert-dcl50-cpp (5): rationale NOLINTs on the printf-style formatters
(a parameter pack cannot produce the va_list vsnprintf needs).
- cppcoreguidelines-pro-type-const-cast: const-correct the WordReader
ctx (API + trampoline + tests, const_cast deleted); NOLINT the two
std <cstring>-convention mutable overloads.
- Long tail: explicit void* casts + sizeof(T) rationale in rtos_queue,
range-for loop conversions, 0x80-mask instead of int8_t reinterpret
for the AXP2101 gauge, uint8_t slot subscript, parenthesized -1 pin
macros, extended the __wrap_log_printf ABI suppression to the
reserved-identifier aliases, integer-center tween targets.
Claude-Session: https://claude.ai/code/session_01SV58JXhfxhhc9DC6vdjBo4
Fix 26 SonarQube code smells (S995/S5817/S3646/S6004/S6023/S6177/S3230)
S995 (12): const-qualify AppContext& parameters in draw/render
functions that only read from context (settings, bond manager, host
select, cred detail, stick vault, tabbed vault, TOTP controller).
S5817 (3): mark commitStepper, enterStickSettingsTree, and
CredDetailController::draw as const member functions.
S3646 (4): split combined struct-definition-plus-instance declarations
in AppContext (Display, VaultSession, MenuModel, BootInfo) into
separate type definition and member declaration statements.
S6004 (1): move opened = credDetail_.open(...) into the if
init-statement in HomeStateHandler::openCredential.
S6023 (2): replace if(auto x=opt){return *x;}return D; with
.value_or(D) in TabbedVaultView::update and StickVaultView::update.
S6177 (2): add using enum AppState; in the hostSelect and credDetail
switch blocks in HomeStateHandler::update to drop AppState:: prefixes.
S3230 (1): move ScopedLock::lock_ from ctor init list to in-class
member initializer in crypto_hw_lock.h.
Not fixed: temperature.cpp S995 (out-param, genuine false positive);
totp_screen_controller S6229 mktime (UTC semantics preserved, std::chrono
swap risks silent timezone change).
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Fix 26 SonarQube code smells (S995/S5817/S3646/S6004/S6023/S6177/S3230)
S995 (12): const-qualify AppContext& parameters in draw/render
functions that only read from context (settings, bond manager, host
select, cred detail, stick vault, tabbed vault, TOTP controller).
S5817 (3): mark commitStepper, enterStickSettingsTree, and
CredDetailController::draw as const member functions.
S3646 (4): split combined struct-definition-plus-instance declarations
in AppContext (Display, VaultSession, MenuModel, BootInfo) into
separate type definition and member declaration statements.
S6004 (1): move opened = credDetail_.open(...) into the if
init-statement in HomeStateHandler::openCredential.
S6023 (2): replace if(auto x=opt){return *x;}return D; with
.value_or(D) in TabbedVaultView::update and StickVaultView::update.
S6177 (2): add using enum AppState; in the hostSelect and credDetail
switch blocks in HomeStateHandler::update to drop AppState:: prefixes.
S3230 (1): move ScopedLock::lock_ from ctor init list to in-class
member initializer in crypto_hw_lock.h.
Not fixed: temperature.cpp S995 (out-param, genuine false positive);
totp_screen_controller S6229 mktime (UTC semantics preserved, std::chrono
swap risks silent timezone change).
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Retire LVGL entirely (HAS_LVGL gone)
The T-Deck was the only LVGL device and now renders through the in-house UI
library (verified on hardware), so every #if HAS_LVGL branch is dead. Remove the
framework wholesale:
- Delete src/ui/lvgl/ (25 files: hal, input, screen_manager, theme, demo,
brand_mark, and the per-screen lvgl_*_screen set) and
src/ui/popup/lvgl_popup_adapter.{cpp,h}.
- Delete src/ui/core/render_task.{cpp,h} (the lv_timer_handler worker + LVGL
recursive mutex; every caller was LVGL-gated) and variants/tdeck/lv_conf.h.
- Delete src/hal/common/display_hal_tft.cpp (the HAS_LVGL && ST7789 backend);
the T-Deck now links display_hal_m5.cpp (BL pin >= 0, identical brightness path).
- De-gate ~25 source files: drop the dead HAS_LVGL branch, keep the live
in-house / HAS_KEYBOARD branch.
- Remove the HAS_LVGL macro (variants/tdeck/variant.h and the platform_defaults.h
default) and the lvgl/lvgl @ 9.2.2 lib_dep + LV_CONF flag in tdeck.ini.
- Update docs (groups.dox, building, ui/, design-system) and CLAUDE.md /
copilot-instructions to reflect that LVGL is retired fleet-wide.
Font tooling (scripts/fonts lv_font_conv) and descriptive lineage comments
(e.g. "raster replacement for lv_textarea") are kept. Full fleet builds green;
native 1075/1075; pio check clean; no HAS_LVGL/lvgl runtime reference remains.
Extract Stick 2-button vault menu into StickVaultView
Introduce the device-layout view strategy: a VaultMenuView interface + a VaultMenuHost
seam (parent implements openCredential / showTotp / enterAdmin / lockAndSleep /
openSettingsTree / openAboutOverlay, mapping semantic menu events to the controllers
and FSM) in new src/states/vault_menu_view.h. Extract the whole Stick 2-button menu —
the pager-page model, list rendering, scroll + tab-indicator animation, the HOLD-A
progress border, and the 2-button input — out of the VaultStateHandler god-object into
StickVaultView : VaultMenuView (src/states/stick_vault_view.{h,cpp}). Move the shared
row primitives (drawCredentialIcon / drawStatusChip / drawActionItem) to
vault_row_widgets.{h,cpp} and the per-row username/brand caches to a shared CredRowCache
(src/states/cred_row_cache.{h,cpp}), both reused by the still-inline Gray/keyboard rows.
VaultStateHandler holds stickView_ (on !HAS_KEYBOARD, where it also inherits
VaultMenuHost), delegates the Stick draw/update/enter to it, and forwards its
cross-cutting menuRedraw_/menuListOnlyRedraw_ into the view (folding
stickView_.pendingRepaint() into the overlay-repaint check). The Gray tabbed and
keyboard DevicePanel paths stay inline for the next device-view sub-phases.
Behavior-preserving verbatim relocation: the scroll/tab animation cadence, multi-tap
page-flip timing, HOLD-A border segments, and sticky-scroll-window logic are unchanged.
Removed the dead, call-site-less activateListSelection. Drops ~1300 lines from
vault_state.cpp. First device-view sub-phase of the vault-state split.
Verified: native 1168/1168, pio check clean, guards, full fleet -Werror (9 variants), and
on-device QA on Stick S3 + M5StickC Plus2 (the 2-button boards that run the view:
render/nav, page flips + tab slide, HOLD-A border, activations, cross-cutting repaint,
no flicker/shear) + M5Stack Gray & CoreInk (3-button tabbed path unaffected).
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Rename VaultStateHandler to HomeStateHandler (home_state, AppState::Home)
The FSM state handler was misnamed — "vault" belongs to the storage/crypto layer
(src/vault/, VaultStore, VaultCrypto), whereas this handler is the post-unlock home
orchestrator (menu + settings + admin + device config + BLE hosts + cred-detail). Rename
it throughout: class VaultStateHandler -> HomeStateHandler; files
src/states/vault_state.{h,cpp} -> home_state.{h,cpp} (+ the mirror test test_vault_state ->
test_home_state); enum AppState::VaultMenu -> AppState::Home; the debug screen-name strings
"VAULT_MENU/..." -> "HOME/..."; the appStateName() label "VAULT_MENU" -> "HOME"; the
"requires VAULT_MENU" debug messages; and the FSM references in the docs, .github
skills/agents, and QA scripts. The GOTO debug command keeps "VAULT_MENU" as a back-compat
alias alongside the new "HOME".
The src/vault/ storage/crypto layer keeps "vault" (correct usage); the VaultMenuView /
vault_row_widgets / cred_row_cache helpers keep their names (they name the vault-contents
menu, still accurate). Pure rename, no logic change.
Verified: native 1168/1168, pio check clean, guards, full fleet -Werror (9 variants +
debug), and on-device QA on Stick S3 + M5Stack Gray (boot, PIN dial via PinEntryRenderer,
home menu nav, HOME/... state strings). Completes the vault-state god-object split:
HomeStateHandler is device-agnostic and ~30 methods / ~16 fields, under the Sonar
S1448/S1820 thresholds.
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X