Keep the watchdog's real settings, bound the task census, record failed allocations
The task watchdog was permanently downgraded by the first PBKDF2 derivation.
main.cpp configures it with panic-on-timeout enabled, but the guard that widens
the timeout around a long derivation restored a value read from
CONFIG_ESP_TASK_WDT_PANIC, which is not set, so from the first unlock onwards a
wedged loop task would log a backtrace and carry on with the vault open instead
of rebooting. ESP-IDF exposes no getter for the live configuration, so the
platform watchdog facade now owns it: it records what it applied, seeded from the
CONFIG_ESP_TASK_WDT_* values IDF auto-init used, and offers a ScopedTimeout guard
that captures the live settings, raises only the timeout and restores exactly
what it captured. The two duplicated guards under src/crypto/ are gone, so the
defect cannot come back in only one of them.
The task census silently truncated. uxTaskGetSystemState returns zero, not a
partial list, when its array is smaller than the live task count, and the console
capped at 24 while the facade capped at 32. At peak census TASKS would have
printed no rows and looked like a parse failure. One bound now lives in
rtos_task.h, a static_assert stops a caller array from drifting past it, the
console arrays are static so they no longer consume the loop-task stack they
report on, and truncation prints an explicit line naming the live count.
Nothing recorded allocation failures, which is what turned a WPA3 association
failure on classic ESP32 into a long hunt. A new platform facade registers an
allocation-failure hook and writes size, capability mask, calling address, core
and uptime into a small static ring. The hook runs under HEAP_IRAM_ATTR, so it
neither logs, allocates, locks nor touches flash. It ships in every build: the
ring holds integers and code addresses, never memory contents.
Burning eFuses is what separates the hardened targets from the open ones, so the
USB-JTAG fuse is now gated on KLEIDOS_SECURE instead of KLEIDOS_RELEASE. _secure
and _secure_dev inherit that flag and still burn, as their upload guard already
warns; _release goes back to being fully re-flashable, which is what the target
taxonomy always documented.
Verified: sticks3_debug builds clean and native tests pass 2861/2861.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
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.
Keep the watchdog's real settings, bound the task census, record failed allocations
The task watchdog was permanently downgraded by the first PBKDF2 derivation.
main.cpp configures it with panic-on-timeout enabled, but the guard that widens
the timeout around a long derivation restored a value read from
CONFIG_ESP_TASK_WDT_PANIC, which is not set, so from the first unlock onwards a
wedged loop task would log a backtrace and carry on with the vault open instead
of rebooting. ESP-IDF exposes no getter for the live configuration, so the
platform watchdog facade now owns it: it records what it applied, seeded from the
CONFIG_ESP_TASK_WDT_* values IDF auto-init used, and offers a ScopedTimeout guard
that captures the live settings, raises only the timeout and restores exactly
what it captured. The two duplicated guards under src/crypto/ are gone, so the
defect cannot come back in only one of them.
The task census silently truncated. uxTaskGetSystemState returns zero, not a
partial list, when its array is smaller than the live task count, and the console
capped at 24 while the facade capped at 32. At peak census TASKS would have
printed no rows and looked like a parse failure. One bound now lives in
rtos_task.h, a static_assert stops a caller array from drifting past it, the
console arrays are static so they no longer consume the loop-task stack they
report on, and truncation prints an explicit line naming the live count.
Nothing recorded allocation failures, which is what turned a WPA3 association
failure on classic ESP32 into a long hunt. A new platform facade registers an
allocation-failure hook and writes size, capability mask, calling address, core
and uptime into a small static ring. The hook runs under HEAP_IRAM_ATTR, so it
neither logs, allocates, locks nor touches flash. It ships in every build: the
ring holds integers and code addresses, never memory contents.
Burning eFuses is what separates the hardened targets from the open ones, so the
USB-JTAG fuse is now gated on KLEIDOS_SECURE instead of KLEIDOS_RELEASE. _secure
and _secure_dev inherit that flag and still burn, as their upload guard already
warns; _release goes back to being fully re-flashable, which is what the target
taxonomy always documented.
Verified: sticks3_debug builds clean and native tests pass 2861/2861.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Finish the admin portal surface and give destruction one bar everywhere
The six endpoint groups the portal rendered nothing for are now firmware,
one module per file, each behind the auth policy its usage demands
(polled reads are SessionReadOnly so a dashboard cannot hold the radio up):
- admin_system_api: /api/system{,/partitions,/hardware}, selftest, beep,
reboot and sleep through the FSM flag seam. Hardware rows carry PROBED
chip identities published from Core 1 through atomics — a build flag
selects a code path, it does not name a part (the StickS3 PMIC lesson).
- admin_totp_api: live one-time codes gated on clock trust before any
vault I/O, answering the whole validity window (remaining_s) so the
page polls once per period instead of once per second.
- admin_credentials_api: the list gains user/favorite/order/hasTotp, and
every delete — single or batch — is prepare (unlock secret) → commit
(one-shot nonce), executed as ONE meta commit and ONE index rewrite by
VaultStore::deleteCredentialBatch. The bare DELETE route is gone.
- admin_ble_api: device name + per-bond rename (POST, not PATCH — the
HTTP facade deliberately stays GET/POST/DELETE), with a bounded
name-store flush so a rename survives a power cycle.
- SD restore wizard: stage → gated commit (unlock secret + imported PIN +
X-Confirm-Restore) → idempotent abort, with foreign work factors
clamped to [1000, 4x local] and session teardown on any failure past
the point of no return. The ungated one-shot import route is removed.
- admin_telemetry_api: on-demand battery sampler with a columnar chunked
body, ownership-guarded so a portal claim can never stop a
debug-console capture, drained on the UI core with a bounded loop.
Destruction now has one bar everywhere: credentials, TOTP entries (new),
SD keyfile/format, wipe, factory reset and the restore commit all demand
proof of the unlock secret through admin_confirm.h. BLE bonds stay the
deliberate exception — re-pairing is recoverable.
The device side gains the owner-facing half of the pinned admin network:
a "Fixed admin network" toggle (OptionId::AdminNetworkFixed, key
ap_fixed) that arms a pin request; the password is still drawn only in
generateApPassword(), before esp_wifi_start(), where the bootloader
entropy source is sound — the toggle records intent, the draw stays in
its one proven-safe place.
The SPA follows every contract (columnar telemetry, the restore 401-with-
restored:true success screen, per-id delete outcomes), the mock mirrors
them, and portal_checks gains seven second-wave HIL checks including
"the retired routes stay retired" and "secretless destruction is refused
before any KDF".
The batch pays its own DRAM bill: the fleet build caught core2_v13_debug
overflowing dram0 by 344 bytes (classic-ESP32 debug links with a few
hundred bytes of headroom). Three trims inside the new modules return
~405 bytes of permanent .bss: the telemetry channel registry is sized to
what the fleet registers (8 slots, not 24), the staged delete batch is
heap-allocated on first use, and the device-information observations
(power snapshot, chip probes, storage figures) live in one heap block
allocated on first touch — they are session observations, not boot
state. The 32 KB vault state and 12.5 KB app context stay deliberately
static: key material must not reach PSRAM, and their fixed reservation
is the linker's honest canary.
Native: 2705/2705. Builds: full 43-env fleet (the two native test envs
that pio run cannot build alone are the known Unity flake), then
core2_v13_debug, sticks3_debug, m5stickc_plus1_debug and core2_v13
re-verified after the DRAM trims. Guards and pre-commit hooks all green.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Finish the admin portal surface and give destruction one bar everywhere
The six endpoint groups the portal rendered nothing for are now firmware,
one module per file, each behind the auth policy its usage demands
(polled reads are SessionReadOnly so a dashboard cannot hold the radio up):
- admin_system_api: /api/system{,/partitions,/hardware}, selftest, beep,
reboot and sleep through the FSM flag seam. Hardware rows carry PROBED
chip identities published from Core 1 through atomics — a build flag
selects a code path, it does not name a part (the StickS3 PMIC lesson).
- admin_totp_api: live one-time codes gated on clock trust before any
vault I/O, answering the whole validity window (remaining_s) so the
page polls once per period instead of once per second.
- admin_credentials_api: the list gains user/favorite/order/hasTotp, and
every delete — single or batch — is prepare (unlock secret) → commit
(one-shot nonce), executed as ONE meta commit and ONE index rewrite by
VaultStore::deleteCredentialBatch. The bare DELETE route is gone.
- admin_ble_api: device name + per-bond rename (POST, not PATCH — the
HTTP facade deliberately stays GET/POST/DELETE), with a bounded
name-store flush so a rename survives a power cycle.
- SD restore wizard: stage → gated commit (unlock secret + imported PIN +
X-Confirm-Restore) → idempotent abort, with foreign work factors
clamped to [1000, 4x local] and session teardown on any failure past
the point of no return. The ungated one-shot import route is removed.
- admin_telemetry_api: on-demand battery sampler with a columnar chunked
body, ownership-guarded so a portal claim can never stop a
debug-console capture, drained on the UI core with a bounded loop.
Destruction now has one bar everywhere: credentials, TOTP entries (new),
SD keyfile/format, wipe, factory reset and the restore commit all demand
proof of the unlock secret through admin_confirm.h. BLE bonds stay the
deliberate exception — re-pairing is recoverable.
The device side gains the owner-facing half of the pinned admin network:
a "Fixed admin network" toggle (OptionId::AdminNetworkFixed, key
ap_fixed) that arms a pin request; the password is still drawn only in
generateApPassword(), before esp_wifi_start(), where the bootloader
entropy source is sound — the toggle records intent, the draw stays in
its one proven-safe place.
The SPA follows every contract (columnar telemetry, the restore 401-with-
restored:true success screen, per-id delete outcomes), the mock mirrors
them, and portal_checks gains seven second-wave HIL checks including
"the retired routes stay retired" and "secretless destruction is refused
before any KDF".
The batch pays its own DRAM bill: the fleet build caught core2_v13_debug
overflowing dram0 by 344 bytes (classic-ESP32 debug links with a few
hundred bytes of headroom). Three trims inside the new modules return
~405 bytes of permanent .bss: the telemetry channel registry is sized to
what the fleet registers (8 slots, not 24), the staged delete batch is
heap-allocated on first use, and the device-information observations
(power snapshot, chip probes, storage figures) live in one heap block
allocated on first touch — they are session observations, not boot
state. The 32 KB vault state and 12.5 KB app context stay deliberately
static: key material must not reach PSRAM, and their fixed reservation
is the linker's honest canary.
Native: 2705/2705. Builds: full 43-env fleet (the two native test envs
that pio run cannot build alone are the known Unity flake), then
core2_v13_debug, sticks3_debug, m5stickc_plus1_debug and core2_v13
re-verified after the DRAM trims. Guards and pre-commit hooks all green.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r