Extend the record to its final shape, revive the dead panels, honor the index
Three firmware waves of the gap-report program, built in parallel
worktrees against the same tip and integrated with both-sides conflict
resolution (two stitch errors — an unopened Doxygen comment and a lost
closing brace — were caught by the compiler, as intended).
Wave 1 — the record's final shape. CredentialRecord gains flags (bit0 =
auto-submit: username, TAB, password, ENTER as one BLE action — and the
trailing ENTER is now conditional where it used to be unconditional),
three typed custom fields (label/value/hidden — hidden values ride
behind the same plaintext-consent header as passwords), a group id, and
TOTP entries gain an account label plus the algorithm byte everywhere.
Groups are a 100-slot encrypted name table (one littlefs block) managed
over GET/POST/DELETE /api/groups — deleting a group only clears labels,
never credentials. The index row carries group, a derived domain column
and real mtime, with names widened to 32 chars; brand is populated at
save time from the url, ending the per-row decrypt the device paid for
logos. URL capacity rises to 192 (the owner's real data maxed at 190),
and every save/import reports per-field truncation instead of silently
clamping. Backup, restore, the normalized importer and the CSV importer
round-trip all of it; the importer maps 1Password vaults and CSV
folder/tags/vault columns into groups server-side.
Wave 2 — six dead routes live: /api/ota/status (the whole Firmware
panel keyed on it), /api/vault/reindex, /api/time/diag (RTC coin-cell
health), the GPS trio (gated to boards that have one), NTP-sync-on-exit
through the AdminMode teardown choke point, and the staged-SD firmware
apply behind the confirm gate. Both full-replace restores now demand
the unlock secret (X-Unlock-Secret) — a backup passphrase proves
nothing about owning the live vault — and the restore body cap is
computed from the device's own backup bound instead of a 64 KB literal.
Wave 3 — the UI stops lying: the audit export honors Accept: text/csv
with real CSV, the web CSV importer honors the type column (Bitwarden
non-logins skipped and counted), LastPass and Proton Pass get real
header signatures, /api/runtime-stats is gone, AutoLockMode appears on
the six boards that hid a working handler, and the device vault lists
honor favorite-first plus manual order with an allocation-free
comparator.
Integration extras: the index envelope buffers and the rekey crypto
scratch now live on the heap (the rekey frame was already past the
32 KB worker stack at the current 200-slot caps — the latent overflow
the dimensioning study flagged); core2_v13's capacity-scaled view-cache
block moves to PSRAM .ext_ram.bss (keys and passwords stay in internal
DRAM), un-breaking its 48-byte link margin; the groups list
distinguishes a corrupt table from an empty one; custom:null is refused
rather than destructive; and the three amalgam suites learned the new
seams, including rebasing an unreachable handler-413 assertion onto the
gate-logic suite that actually pins the bound.
Builds: sticks3_debug, tdeck, cardputer, m5stickc_plus1_debug,
core2_v13_debug (now with SPIRAM bss). Native: full suite green with
nine new suites registered.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Make the credential the full object the owner defined, everywhere
The ruling: a credential is user + password + web + notes + TOTP, the web
admin is the superset surface, and no surface may destroy what another
one wrote. This lands the investigation's NOW tier end to end.
The seam that closes a bug class: VaultStore::saveCredential now
read-modify-writes the stored extras, so a bare save from ANY surface —
the portal form, the device editors, a future caller — preserves notes,
brand and the embedded TOTP instead of silently rebuilding the record
from defaults. The portal edit no longer wipes notes (the GET now emits
them — behind the same X-Confirm-Plaintext consent as the password,
since notes hold recovery codes), and the device editors were moved off
the destructive path.
The TOTP write path exists at last: POST /api/credentials accepts an
optional totp object (base32 or a full otpauth:// URI; explicit null
detaches), the SPA credential form gains its 2FA section, and standalone
and embedded TOTP convert both ways. TOTP records gain an algorithm byte
(SHA-1 default, SHA-256 for the issuers that mandate it) verified
against the RFC 6238 Appendix B vectors, and every generate() call site
passes it.
Backups stop lying: exportAll/importAll round-trip the complete record —
notes, embedded TOTP, favorite, order, brand, timestamps — so the
portable paths (/api/backup, .kexp, SD) finally match what the page
promises. Credential saves stamp ctime/mtime from the trusted epoch when
the clock deserves it.
The 1Password import is real: the SPA import wizard parses .1pux in the
browser (a self-contained ZIP walker over DecompressionStream — the
12 MB export never touches the device; only export.data is read) and
vendor CSVs (1P8's Url header now matches; OTPAuth/Notes/Favorite/
Archived columns mapped), previews with per-vault selection, capacity
metering, dedup marks and an archived-skip toggle, then uploads through
the new POST /api/import/items — a merge importer that batches 32 items,
generates 20-character passwords on-device for passwordless rows when
asked, and reports per-item outcomes. Two flows that were dead on real
hardware come back with one-line fixes each: rekey read "new" where the
page sends "next", encrypted restore read X-Export-Passphrase where the
page sends X-Backup-Pass.
The drift class those two bugs belong to now has a test: qa/tests/portal/
test_spa_contract.py walks every SPA fetch against the firmware route
table, the mock and a key-name fixture (17 checks). The fixtures' two
documented drift entries were resolved the way they demanded.
The compressed-SPA budget rises to 64 KB by owner decision — the portal
is the device's full-featured surface now, and even the tightest 4 MB
board carries it with room (all five representative builds pass,
including both classic-ESP32 debug canaries).
Native: 2758/2758. Contract: 17/17. Builds: sticks3_debug, tdeck,
cardputer, m5stickc_plus1_debug, core2_v13_debug. Guards and hooks green.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Extend the record to its final shape, revive the dead panels, honor the index
Three firmware waves of the gap-report program, built in parallel
worktrees against the same tip and integrated with both-sides conflict
resolution (two stitch errors — an unopened Doxygen comment and a lost
closing brace — were caught by the compiler, as intended).
Wave 1 — the record's final shape. CredentialRecord gains flags (bit0 =
auto-submit: username, TAB, password, ENTER as one BLE action — and the
trailing ENTER is now conditional where it used to be unconditional),
three typed custom fields (label/value/hidden — hidden values ride
behind the same plaintext-consent header as passwords), a group id, and
TOTP entries gain an account label plus the algorithm byte everywhere.
Groups are a 100-slot encrypted name table (one littlefs block) managed
over GET/POST/DELETE /api/groups — deleting a group only clears labels,
never credentials. The index row carries group, a derived domain column
and real mtime, with names widened to 32 chars; brand is populated at
save time from the url, ending the per-row decrypt the device paid for
logos. URL capacity rises to 192 (the owner's real data maxed at 190),
and every save/import reports per-field truncation instead of silently
clamping. Backup, restore, the normalized importer and the CSV importer
round-trip all of it; the importer maps 1Password vaults and CSV
folder/tags/vault columns into groups server-side.
Wave 2 — six dead routes live: /api/ota/status (the whole Firmware
panel keyed on it), /api/vault/reindex, /api/time/diag (RTC coin-cell
health), the GPS trio (gated to boards that have one), NTP-sync-on-exit
through the AdminMode teardown choke point, and the staged-SD firmware
apply behind the confirm gate. Both full-replace restores now demand
the unlock secret (X-Unlock-Secret) — a backup passphrase proves
nothing about owning the live vault — and the restore body cap is
computed from the device's own backup bound instead of a 64 KB literal.
Wave 3 — the UI stops lying: the audit export honors Accept: text/csv
with real CSV, the web CSV importer honors the type column (Bitwarden
non-logins skipped and counted), LastPass and Proton Pass get real
header signatures, /api/runtime-stats is gone, AutoLockMode appears on
the six boards that hid a working handler, and the device vault lists
honor favorite-first plus manual order with an allocation-free
comparator.
Integration extras: the index envelope buffers and the rekey crypto
scratch now live on the heap (the rekey frame was already past the
32 KB worker stack at the current 200-slot caps — the latent overflow
the dimensioning study flagged); core2_v13's capacity-scaled view-cache
block moves to PSRAM .ext_ram.bss (keys and passwords stay in internal
DRAM), un-breaking its 48-byte link margin; the groups list
distinguishes a corrupt table from an empty one; custom:null is refused
rather than destructive; and the three amalgam suites learned the new
seams, including rebasing an unreachable handler-413 assertion onto the
gate-logic suite that actually pins the bound.
Builds: sticks3_debug, tdeck, cardputer, m5stickc_plus1_debug,
core2_v13_debug (now with SPIRAM bss). Native: full suite green with
nine new suites registered.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r