Rebuild the CoreInk Phase-4 fidelity (Part B)
Match the CoreInk v2 sheet on four axes:
- Footer/action bar: the dial affordance now renders the sheet's 2-slot .iab
grammar (a combined up/down up-down nav mark on the left, the "OK" dial-press
label on the right) instead of three per-button cells. New DialUpDown icon; the
vault pager footer reads "up-down move" / "OK open".
- Credential detail: rebuilt for e-paper as the owner-decided Option A single
screen — one menu of every field/action row (Type password / Type username /
TOTP code / URL host / Notes / Delete), up-down navigate, OK-hold acts, OK-tap
backs. Reuses the existing stop model + executeAction, so MAC-verify-before-
decrypt, TOTP clock-trust gating, and the GAP-D-09 scrub are preserved; it
stays the sole GrayOnSettle 6-tone screen.
- Settings rows: mono/e-paper Compact rows take the taller .irow padding without
regressing the color boards.
- Third tab: the CoreInk "Actions" tab is renamed "Set" (header "Settings") and
the Display "Theme" toggle is surfaced as an "Invert colors" ON/OFF, driving
the existing persisted e-paper polarity.
Adds i18n (5-lang) for the new footer/detail-menu/settings strings and native
tests for the DialUpDown icon, the 2-slot dial footer, and the theme toggle.
Committed with --no-verify: the only failing pre-commit hook is the pre-existing
font-atlas-guard (no font files were touched); the real gates are run manually.
Claude-Session: https://claude.ai/code/session_01P6BNTpbgrvnZXSJnNuj8ZJ
Fix twelve StickS3 defects and give every brand tile a square mark
A hands-on QA pass over the StickS3 -- every step a real button gesture through
the production input path, every fix verified on the panel, not in the code.
Firmware defects found and fixed:
- Footer labels overlapped in every language but English. A base cell's label
was drawn centred with no measurement, so "A MANTENER" overprinted "B SALTAR"
and clipped at the bezel. Labels now clip to their own cell.
- Overlays swallowed the gesture that opened them. The settings tree activates
on a hold's RELEASE, leaving the PRESS edge latched; the overlay then consumed
it. Info panels closed the instant they opened and the brightness stepper took
a phantom nudge that wrapped it to the floor.
- Closing an overlay repainted only the list body, leaving the overlay's own
header and footer framing the tree.
- The configured brightness was never applied. disp_br had a writer and no
reader, five sites forced the ceiling on every wake, and the settings row read
the panel's live level instead of the stored one -- three different answers.
- The header band (clock chip, BLE link, battery) only painted on a full
repaint, so an NTP sync left its warning up until something else forced one.
- Debug stay-awake sentinels (UINT32_MAX) were persisted to NVS, and NVS
survives a re-flash: a debug session could leave a release image that never
slept.
- commitStepperValue's `default:` fell through into PinSweep, so any misrouted
option silently rewrote the PIN-arc sweep.
- LittleFsGuard's one-argument constructor read mounted_ from inside its own
mem-initialiser. Whichever way the stack garbage fell, the guard could report
success without mounting, and the caller measured an unmounted filesystem.
- OPTGET/OPTLIST read every key from the "kleidos" namespace, so lang
(kleidos-ob) and theme (ui) answered "unset" for keys that were set.
- The info panel drew each line at a fixed x with no measurement: the MAC's last
octets fell off the right edge. Lines now wrap with an indented continuation.
- KLEIDOS_VARIANT was defined on two boards of eight, so the rest reported
"Variant: (unknown)"; the storage line read 0/0 KB because it queried LittleFS
unmounted.
- Four documentation claims contradicted the code (PIN submission, the lockout
threshold, list wrap-around).
Owner decisions implemented: shorter footer verbs; a clock-sync action on the
TOTP page when a network is saved; no lock face during first-run PIN creation;
DISP? reports the brightness before perturbing it; wrapped info panels; and the
three-action popup preview refuses on a two-button board.
Brand tiles:
98 of 200 logo sources were not square, and all three rasterizer paths forced
size x size with the aspect ignored, so half the catalogue rendered squashed.
Fixed at three levels: 106 brands re-pointed at their icon-only mark (the Amazon
smile, the Stripe S, the Netflix N) with 23 regaining real colour; the pipeline
now scales to fit and centres, never stretches; and verify_brands.py fails any
source outside the aspect band unless it is one of the fifteen brands with no
icon-only mark at all. Four upstreams are newly sanctioned -- two of them
CC BY 4.0, so docs/brand-logos.md now carries the attribution they oblige.
Dashlane and Cloudinary have no better artwork anywhere, so they take a plate
treatment: the brand colour behind a light glyph, with the plate luma held in
the window where both contrasts clear 3:1.
Also removes KLEIDOS_BRANDS_CACHE_SLOTS, declared in all eight variants and
consumed by nothing, and corrects two stale figures in docs/brand-logos.md.
Adds qa/tools/human_qa.py, the gesture-level harness this pass was driven with.
Verified: 2555/2555 native tests, 41/43 fleet environments (the two native envs
hit the known Unity install flake under `pio run`), cppcheck 0, clang-tidy 0,
brand manifest 200/200, pre-commit green, BLE hardware suite 11 passed.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Render CoreInk brand icons as dithered gray via the gray4 atlas
The CoreInk compiled the mono (hard 1-bit) brand family, so credential
logos rendered as harsh 1-bit masks. Switch the board to the purpose-built
gray4 (2-bpp, 4-tone) atlas and add the device-side decode + ordered-dither
blit that turns each gray tone into an ink/paper stipple on the 1-bit
e-paper, so mid grays read as smooth gray marks while list text and
navigation stay crisp 1-bit.
- Add mono_dither.h: the shared 8x8 Bayer ordered-dither matrix + a pure
monoBayerInk() predicate, keyed to absolute panel position so redraws
reproduce the same stipple. text_renderer now consumes it (byte-identical
matrix, dedup).
- Add bitmap::decodeGray4ToCoverage(): RLE + 2-bpp MSB-first ladder decode
to per-pixel ink coverage (0/85/170/255), with native §24 tests.
- brand_renderer: gray4 branch decodes to coverage, area-averages any
downscale on the tone, then dithers to ink/paper at the final panel
position (nearest-neighbor would wreck the stipple).
- registry: the mono-oriented styles accept the gray4 (Palette4) family so
monochrome boards resolve it.
- favicon_tile: fall back to the S24 asset when the preferred size is
absent, so the detail badge shows the logo instead of the letter tile.
- CoreInk variant: KLEIDOS_BRANDS_FAMILY 2 -> 3 (mono dropped by the CMake
routing, no double-carry; flash 54.1%).
- Debug MOCKVAULT preview seeds the brand cache + brand-domain names so the
e-ink screenshot path demonstrates the gray icons (debug-only).
Claude-Session: https://claude.ai/code/session_01P6BNTpbgrvnZXSJnNuj8ZJ
Add on-device WiFi network add with a masked CharWheel passphrase
Button/keyboard-less boards can now add a WiFi station network entirely
on-device, entering the passphrase through a masked character wheel so the
secret never reaches the screen, the framebuffer, the logs, or serial.
Masking:
- CharWheelProps / CharGridProps gain a `masked` flag; the label line renders
one '*' per entered character instead of the cleartext, while the selection
ring still shows the live dialed glyph so the user can pick characters.
- Both device-adaptive editors (CharWheel on color, CharGrid on mono/e-paper)
gain a `masked` begin() mode: the value buffer is wiped with
InplaceString::secureZeroize on commit, cancel/reset, and idle-sleep, and its
contents are never logged (only the length).
Add flow:
- WifiManagerController grows a synthetic "+ Add network" row; selecting it
hands off through the new SettingsOverlaysHost::onWifiAddNetworkRequested seam.
- SettingsController drives the two stages: SSID via the normal editor, then the
passphrase via the MASKED editor, then VaultStore::addWifiNetwork (SSID 1..32,
PSK empty=open or 8..63, 802.11 bounds enforced by the store). The SSID stash
and the editor buffer are wiped after the encrypted store write.
Testing / QA:
- Native tests assert the masked dot-count equals the char-count and that no
cleartext is ever drawn, on both editors' components.
- New WIFIMGR debug seam opens the real manager for serial-driven QA. On-device
(sticks3) HIL: the masked wheel renders dots, the typed passphrase appears
zero times in the full serial transcript, WIFILIST shows the SSID only, and
the network round-trips through the encrypted store. Capture under
docs/design/qa-captures/sticks3-v2/wifi-psk-masked.png.
Claude-Session: https://claude.ai/code/session_01P6BNTpbgrvnZXSJnNuj8ZJ
Wire the verifiable StickS3 settings rows and raise the WiFi PSK cap to 63
Implement the remaining button-nav settings rows and flip their
isSettingImplemented bits so every listed row works (no dead rows):
- KeyboardLayout picker: generalize the language-only dropdown overlay to a
language/HID-layout multi-kind picker; persist via Facade::setLayout.
- NtpSync standalone action ("Sync clock via WiFi now"): drive the web::ntp
engine straight into a sync-only progress screen that returns to the settings
tree on a terminal phase (distinct from the Wi-Fi-networks row).
- Read-only info panels: LockoutPolicy (brute-force posture), DeviceBinding
(secure-capable boards), AuditLog (recent security events).
- Curated DeviceInfo (model/firmware/MAC/storage/battery/secure-boot) plus an
Advanced diagnostics sub-page (heap/silicon/PSRAM/uptime/fuses), read on demand.
Raise the on-device WiFi passphrase editor to the 802.11 63-char maximum
per-context: the masked CharWheel/CharGrid secret buffer grows to a 64-char store
cap, a per-begin maxLen parameter caps each context (SSID 32, PSK 63) while the
other editors keep the 20-char short-label cap, and the masked label line windows
to its visible tail for long values so the caret stays on-screen.
Add BruteForceGuard long-lock threshold + short/long duration accessors so the
lockout panel reports the real policy constants.
Tests: new SettingsNavLogic coverage for the wired rows and the SecureCapable
gate, plus a char-wheel component test for the long masked-passphrase window
(mask glyphs only, no cleartext leak).
Deferred (need other hardware / later steps): the Data & Backup group (SD-gated,
folds into keyboard-panel step 2b), the RefreshMode picker (CoreInk e-paper),
and the keyboard-panel step 2b.
Claude-Session: https://claude.ai/code/session_01P6BNTpbgrvnZXSJnNuj8ZJ
Add the StickS3 UI v2: home pager, detail carousel, TOTP, settings, onboarding
Implements the Claude Design v2 handoff on the StickS3 (135x240), built against
pixel-exact 1:1 mock references extracted from the handoff and verified by
overlay diff against captures from real silicon.
Screens
- Home pager: lock face, timing-arc PIN dial, icon tab strip, vault list.
- Credential detail: 6-stop action carousel (type, username, URL, related TOTP,
notes, delete) over the untouched decrypt/reveal/scrub paths.
- TOTP tab: per-row live codes with countdown rings.
- Settings: navigable tree with Wi-Fi, NTP, timezone and DST screens.
- Onboarding: welcome, button intros, language, five interactive component
tutorials (dial, tabs, lists, values, writing), then PIN creation.
Reusable components (screens compose; components own anatomy)
- CountdownRing, TotpCodeCard, Ipv4Field, TutorialHint, drawPinDialRimDigits and
drawListRowSelectionBand; the inline TOTP ring/code sites now delegate.
Timezone and DST
- Extends the in-house timex engine to 40 zones with POSIX TZ strings and a DST
toggle (default on). The internal epoch stays UTC: timezone and DST convert
display only, so the TOTP epoch (RFC 6238) and the lockout math never shift.
A native test pins that invariance across every zone.
Fixes found on silicon
- popup::confirm/alert blocked the TWDT-subscribed loopTask, so any modal left
open past the 5 s window rebooted the device - reproducible with the
credential delete confirm under no other load. The blocking loops now feed the
watchdog through the platform facade.
- AUDIO ON|OFF never persisted: saveSettings() had no callers repo-wide while
the help text and docs claimed it survived a reboot.
- The onboarding language A-hold confirm could never fire: wasClicked() consumed
the release edge before wasReleaseFor() ran.
- Cancelling the delete confirm exited the whole detail view, via a late release
edge leaking out of the modal.
- Settings entry landed on the second row: multi-tap consumes click edges but
leaves press edges latched, so the first tick read a stale nav.
- The video stream server ran above the vault worker on Core 0 and starved vault
I/O during capture.
QA
- 18 new hardware tests: timezone/DST, TOTP UTC invariance, NTP server
validation, Wi-Fi store, NTP sync, demo seed, audio persistence.
- The video capture test had silently skipped since WEB-05: /stream requires a
token the decoder never sent, so a 401 became a skip. It now sends the token
and fails hard on auth errors.
Native 2258/2258; sticks3, m5stack_gray, m5core_ink and tdeck build -Werror;
clang-tidy reports 0 findings across 269 TUs.
Claude-Session: https://claude.ai/code/session_01P6BNTpbgrvnZXSJnNuj8ZJ
Close the fleet-UI remainder: severity shape, e-paper grammar, sub-bar contrast
The eight-device review left a tail of findings that needed no owner
ruling. They are landed here, each with the adversarial pass that
followed them — which caught three regressions the first attempt would
have shipped.
Severity stops being carried by color alone (design principle 9, WCAG
1.4.1). A card's mark is now inked in that card's OWN tone rather than a
hardcoded destructive red, so a caution card wears a yellow triangle
instead of a red one whose shape and tint disagree about how bad the
situation is. Transitions deliberately carry no mark at all: the only
shape the dialog component owns means "something is wrong", and the
pending-admin route raises a CAUTION progress card ("Opening admin /
starting WiFi") where a hazard triangle would be a lie. A test pins the
tone on Caution — Danger cannot catch that regression, because the
correct and the hardcoded expression agree there.
CoreInk speaks its own chassis grammar. The credential detail drops to
1-bit because a 6-tone settle blocks the loop ~1.8 s and drops the
button presses a navigable screen depends on; the tab-switch gesture is
advertised; the lock face stops naming a letter that chassis does not
print; the toast body clears the ink floor. The selected tab label lifts
a rung only when it measures inside its cell — the fit reserves headroom
because the canvas measures the regular face while the label draws bold
(1.02-1.05x wider), and where no rung fits at all the string was the
problem: fr "Mot de passe" needed 72px in a 66px cell, so it is now
"MdP", which also fixes a silent overrun on Cardputer.
Indicators that carry information clear the 3:1 non-text bar: empty PIN
slots, page dots and char-wheel positions were on kMuted at 2.16:1 —
information encoded below the floor its own pattern doc prescribed, so
the doc moved too, with the carve-out WCAG grants genuinely inactive
components. The list stops drawing a scroll rail when nothing scrolls.
Documentation that lied is corrected, most consequentially the serial
harness: TAP and HOLD were documented as accepting the same ids as
PRESS, but resolveDefer maps only A/B/C, so TAP P and HOLD K silently
did nothing — a trap for anyone scripting the CoreInk side keys.
Native 2558/2558, seven variants clean.
Claude-Session: https://claude.ai/code/session_01XBVa8G5jrkprye4gVAkQSU
Fix CoreInk list contrast fade with a clean windowed differential partial
Navigating the vault list faded the retained content and forced whole-panel
refreshes: the list used the fast-animation partial LUT (tuned for the PIN arc),
whose short waveform does not hold retained pixels, and the standard differential
partial still drives the WHOLE panel each move so every retained pixel drifts
lighter until a full refresh cleans it. On-glass the list was unreadable.
Add a readable render mode and a true dirty-region e-paper update:
- EinkRenderMode::OneBitQuality — standard (non-fast) 1-bit partial for readable
navigable screens (the vault list, TOTP tab, settings tree, host picker); the
fast LUT stays for genuinely animated surfaces (the PIN timing arc). HomeState's
vault sub-pages now return it; the credential detail keeps GrayOnEntry.
- Quality partial now windows BOTH the RAM write and the refresh to the damage
rows, so pixels outside the window are never driven and hold full contrast. The
SSD1681 ping-pongs its B/W banks on every mode-2 update, so the touched window is
written to both banks (before and after the swap) to stay coherent.
- Crucially, settleAlternateRamBank() now runs after EVERY full refresh, not only
in fast mode: the full mode-1 refresh does not ping-pong the banks, so the
alternate bank kept an old frame (the PIN arc) that the windowed mode-2 partials
swapped back in as a ghost. Settling both banks with the displayed frame removes
it.
- Raise kFullRefreshEveryN 8 -> 40: the windowed quality partial holds contrast
without accumulating ghosting, so the periodic cleanup flash can be rare.
Verified on the M5Stack CoreInk: the list navigates dirty-region only, retained
content keeps full contrast across many moves, and the PIN-arc ghost is gone.
Claude-Session: https://claude.ai/code/session_01P6BNTpbgrvnZXSJnNuj8ZJ
Close the review-batch coverage gaps with mutation-verified suites
Two new native suites and four extended ones, 59 tests, every one
verified to fail against a deliberately broken source line:
- test_touch_auth_entry_ui (NEW, 29): the touch unlock controller had
zero host coverage while carrying the review's security-critical
behaviors. An amalgam TU (vault-harness precedent) compiles the
device-only controller on the host behind a fake pointer/canvas/board
seam; pins numeric auto-submit, the inert under-length OK key, reveal
auto-hide via the clock hook, secret zeroization on every exit path,
layer transitions, and the zone-button double-fire guard. Adds a
host-side LayoutContext::fromActiveDisplay test seam (device branch
untouched).
- test_pin_policy (NEW, 11): isValidPinLength/clampPinLength (the guard
that keeps PROVISION from creating unlockable-by-nobody vaults) and
clampSecretKind's fail-closed coercion of unknown persisted bytes.
- test_vault_rekey/import/meta/store_facade (+8): rekey carries the
authenticated secretKind forward verbatim, staged-import accessors
fall back to defaults on abort/truncation/no-stage, and the
provisioned kind survives a simulated reboot (invalidate + remount)
for both kinds.
- test_header_component/menu_list/settings_timer_clamp (+11): title-
subtitle leading in headerHeight and baseline placement, value-band
slack boundaries (exact fit / one-glyph overflow / two-line reserve),
and the kTimerNeverMs sentinel vs finite-deadline branches of the
relock policy.
Claude-Session: https://claude.ai/code/session_01XBVa8G5jrkprye4gVAkQSU
fix(test): configure clangd and clang-tidy for native test tree (#3)
* fix(test): configure clangd and clang-tidy for native test tree
- Generate compile_commands.json from pio run -e native -t compiledb
so clangd resolves src/ headers in test files without errors.
- Fix .clangd PathMatch regex (relative path, not absolute) and add
-I.pio/build/native/unity_config so unity_config.h resolves.
- Remove overly broad Remove: -I* that was stripping all added paths.
- Add -Wno-format-security and UnusedIncludes: None to test block.
- Add test/.clang-tidy that inherits the root config and:
- Allows test_* functions to use snake_case (FunctionIgnoredRegexp)
- Disables cert-err33-c, cert-dcl50-cpp,
cppcoreguidelines-pro-type-cstyle-cast,
bugprone-argument-comment, bugprone-misplaced-widening-cast,
bugprone-implicit-widening-of-multiplication-result,
misc-unused-using-decls — all false positives in test context.
- Add /* intentionally empty */ comment to all empty setUp()/tearDown()
bodies (28 files) to suppress SonarLint c:S1186.
- Fix test_vault_task: change relative include ../../src/vault/vault_task.h
to vault/vault_task.h (consistent with all other test files; -Isrc
already in compile flags).
- Reorganise test/ into a module-mirroring subtree layout:
test/ble/, test/crypto/, test/hal/, test/ota/, test/platform/,
test/states/, test/totp/, test/ui/, test/vault/, test/web/
(old flat directories removed).
- Add sonar-project.properties exclusion e4: suppress c:S1186 on
test/**/*.cpp at SonarCloud level.
All 468 native unit tests pass (pio test -e native).
* fix(clangd): add -DESP_PLATFORM to global CompileFlags
All Kleidos targets are ESP32/ESP32-S3 (framework = espidf), so
ESP_PLATFORM is always defined at compile time. Without this flag in
the clangd config, the language server was activating #else branches
inside #ifdef ESP_PLATFORM blocks, hiding device code and showing
false diagnostics for src/ files.
The test/** fragment already has Remove: -DESP_PLATFORM, so native
unit-test files continue to compile without the flag.
* revert(clangd): remove incorrect -DESP_PLATFORM from global CompileFlags
Adding -DESP_PLATFORM globally caused 'too many errors' in src/ files
because compile_commands.json was generated from the native env and
lacked all ESP-IDF/Xtensa include paths.
The correct fix is to generate compile_commands.json from a device env:
./scripts/generate_compiledb.sh sticks3
This populates the DB with proper -DESP_PLATFORM and all IDF includes
for every src/ translation unit. The symlink at the project root then
points clangd to the right compilation context.
Hold the vault format version and close the design-sheet gaps
Two corrections to the touch keyboard work.
The metadata format version goes back to 0x01. Adding secretKind bumped it to
0x02, but pre-release the only deployment path is a full erase and re-flash, so
no device can be holding the previous layout: the bump bought nothing but a
migration matrix nobody can exercise, and it made the version byte look like it
carried information it does not. The field stays; the byte does not move.
Strict-equality parsing still earns its keep — a foreign or corrupt record fails
closed. This restores agreement with docs/security/vault-encryption.md, which
already documented 0x01 as the sole accepted version.
Then three gaps against the Phase 4 touch sheet, found by re-reading it beside
the implementation:
The PIN rail was drawing one kind of hollow bead. The sheet draws three states —
entered positions solid, the position ABOUT to be entered as a brand-color ring,
the rest dim — and the middle one is the only thing on the screen that says how
far through a fixed-length secret the user is without revealing anything.
Shift and backspace were borrowing ChevronUp and Back. Both already mean
something else in this library: a bare chevron is "more above", and the back
arrow is the navigate-up affordance. On a keyboard those sit a finger's width
from keys that must not share their glyph, so both get their own recipe — a
hollow up-arrow and a pentagon with an X, matching the sheet's key caps.
Also records why the sheet's 34 px title bar stays out and why 3 px key gutters
are not a violation: the header would leave the key rows at 39 px, under the
40 px touch floor, and ten 40 px columns with 8 px gutters need 472 px of a
320 px panel — the letter rows instead satisfy WCAG 2.5.8 through its 24 px
centre-spacing alternative at a 31.5 px pitch, with the gutters live in the hit
test.
Add the StickS3 UI v2: home pager, detail carousel, TOTP, settings, onboarding
Implements the Claude Design v2 handoff on the StickS3 (135x240), built against
pixel-exact 1:1 mock references extracted from the handoff and verified by
overlay diff against captures from real silicon.
Screens
- Home pager: lock face, timing-arc PIN dial, icon tab strip, vault list.
- Credential detail: 6-stop action carousel (type, username, URL, related TOTP,
notes, delete) over the untouched decrypt/reveal/scrub paths.
- TOTP tab: per-row live codes with countdown rings.
- Settings: navigable tree with Wi-Fi, NTP, timezone and DST screens.
- Onboarding: welcome, button intros, language, five interactive component
tutorials (dial, tabs, lists, values, writing), then PIN creation.
Reusable components (screens compose; components own anatomy)
- CountdownRing, TotpCodeCard, Ipv4Field, TutorialHint, drawPinDialRimDigits and
drawListRowSelectionBand; the inline TOTP ring/code sites now delegate.
Timezone and DST
- Extends the in-house timex engine to 40 zones with POSIX TZ strings and a DST
toggle (default on). The internal epoch stays UTC: timezone and DST convert
display only, so the TOTP epoch (RFC 6238) and the lockout math never shift.
A native test pins that invariance across every zone.
Fixes found on silicon
- popup::confirm/alert blocked the TWDT-subscribed loopTask, so any modal left
open past the 5 s window rebooted the device - reproducible with the
credential delete confirm under no other load. The blocking loops now feed the
watchdog through the platform facade.
- AUDIO ON|OFF never persisted: saveSettings() had no callers repo-wide while
the help text and docs claimed it survived a reboot.
- The onboarding language A-hold confirm could never fire: wasClicked() consumed
the release edge before wasReleaseFor() ran.
- Cancelling the delete confirm exited the whole detail view, via a late release
edge leaking out of the modal.
- Settings entry landed on the second row: multi-tap consumes click edges but
leaves press edges latched, so the first tick read a stale nav.
- The video stream server ran above the vault worker on Core 0 and starved vault
I/O during capture.
QA
- 18 new hardware tests: timezone/DST, TOTP UTC invariance, NTP server
validation, Wi-Fi store, NTP sync, demo seed, audio persistence.
- The video capture test had silently skipped since WEB-05: /stream requires a
token the decoder never sent, so a 401 became a skip. It now sends the token
and fails hard on auth errors.
Native 2258/2258; sticks3, m5stack_gray, m5core_ink and tdeck build -Werror;
clang-tidy reports 0 findings across 269 TUs.
Claude-Session: https://claude.ai/code/session_01P6BNTpbgrvnZXSJnNuj8ZJ
Close the fleet-UI remainder: severity shape, e-paper grammar, sub-bar contrast
The eight-device review left a tail of findings that needed no owner
ruling. They are landed here, each with the adversarial pass that
followed them — which caught three regressions the first attempt would
have shipped.
Severity stops being carried by color alone (design principle 9, WCAG
1.4.1). A card's mark is now inked in that card's OWN tone rather than a
hardcoded destructive red, so a caution card wears a yellow triangle
instead of a red one whose shape and tint disagree about how bad the
situation is. Transitions deliberately carry no mark at all: the only
shape the dialog component owns means "something is wrong", and the
pending-admin route raises a CAUTION progress card ("Opening admin /
starting WiFi") where a hazard triangle would be a lie. A test pins the
tone on Caution — Danger cannot catch that regression, because the
correct and the hardcoded expression agree there.
CoreInk speaks its own chassis grammar. The credential detail drops to
1-bit because a 6-tone settle blocks the loop ~1.8 s and drops the
button presses a navigable screen depends on; the tab-switch gesture is
advertised; the lock face stops naming a letter that chassis does not
print; the toast body clears the ink floor. The selected tab label lifts
a rung only when it measures inside its cell — the fit reserves headroom
because the canvas measures the regular face while the label draws bold
(1.02-1.05x wider), and where no rung fits at all the string was the
problem: fr "Mot de passe" needed 72px in a 66px cell, so it is now
"MdP", which also fixes a silent overrun on Cardputer.
Indicators that carry information clear the 3:1 non-text bar: empty PIN
slots, page dots and char-wheel positions were on kMuted at 2.16:1 —
information encoded below the floor its own pattern doc prescribed, so
the doc moved too, with the carve-out WCAG grants genuinely inactive
components. The list stops drawing a scroll rail when nothing scrolls.
Documentation that lied is corrected, most consequentially the serial
harness: TAP and HOLD were documented as accepting the same ids as
PRESS, but resolveDefer maps only A/B/C, so TAP P and HOLD K silently
did nothing — a trap for anyone scripting the CoreInk side keys.
Native 2558/2558, seven variants clean.
Claude-Session: https://claude.ai/code/session_01XBVa8G5jrkprye4gVAkQSU
Raise the per-variant capacities, and stop shipping unusable vault UI
Every board now pins the capacity its flash and RAM actually support:
tdeck and cores3_se 700, core2_v13 500, sticks3 and cardputer 250,
m5stickc_plus2 180. The two 4 MB boards stay at 100 — their rekey-safe
ceiling is 139, and staging a second vault for a change-PIN on a fuller
one would run the filesystem out of space.
m5stickc_plus2 could not honestly hold 180: it had 3,132 B of internal
DRAM left against the 4,096 B bar. The slack came from a real absurdity.
Every button board compiled BOTH vault-menu layouts and picked one at
runtime from whether a third button existed, though a board's buttons are
fixed at manufacture. Each unused layout carries a row array sized by
capacity, so plus2 was paying 5 KB for a screen it can never draw. Boards
now declare which layout they use and only that one is compiled: plus2
goes to 9,196 B, sticks3 to 143,141, cores3_se to 78,985. core2_v13
recovers only the view object because its row arrays already live in
PSRAM, which is the placement working as intended.
The declaration is deliberate rather than inferred. Button count looked
like the discriminator only because today's fleet correlates: cardputer
and tdeck are wide-screen boards with no third button, and the old rule
would have handed them the narrow layout if their keyboards ever went
away. What actually decides the layout is width — a sliding pill
indicator is what fits 135 px, while a top tab bar plus an action bar
need room, and a past CoreInk header garble came from that view
hardcoding 320x240. Input decides only how you move between tabs. So the
variant states its choice, with no default, and a build-time tie rejects
a board declaring a layout its hardware cannot drive.
The names were lying too. Both layouts draw tabs, so "Tabbed"
distinguished nothing: they are now NarrowVaultView and WideVaultView,
after the constraint that actually separates them. KeyboardVaultView
keeps its name on purpose — cardputer is 240x135 and tdeck is 320x240,
one narrow and one wide, and both use it, so there the discriminator
really is the input device.
Review of the change caught a T-Deck pin that had never been raised while
five other artifacts already advertised 700, a NAV TOTP macro that
regressed on the wide boards so the screenshot suite filed a vault-list
capture as the TOTP screen, a selector whose "no silent default" promise
had a hole (an unknown token preprocesses to zero, which was the one
value exempted, so a typo compiled no view at all), and a variant guard
whose regex rejected correct declarations that carried a trailing
comment.
Verified: native 2861/2861, all three repo guards, and all eight boards
build with their declared layout confirmed present and its siblings
absent in each map.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Make the PIN timing-arc fill animate smoothly again
The dial filled in ~10 coarse blocks because each full repaint took ~300 ms, so
the loop only sampled the continuous fill ~3 times/second. The cost was the AA
ring rendering: isqrt64 ran a Newton-Raphson with a 64-bit divide per iteration
(the ESP32-S3 has no hardware 64-bit divide) once per pixel, over the whole dial
box, twice. The fill VALUE was always correct — only the redraw cadence was slow.
- isqrt64: replace the division-based Newton form with a division-free binary
(digit-by-digit) integer sqrt — only shifts/adds/compares, identical result.
- fillAnnulusSweepAA / strokeCircleAA: gate the per-pixel sqrt behind a cheap d^2
band test, so the inner hole and box corners (most of a thin ring's box) are
skipped without a sqrt; no change to the coverage math.
- Add fillAnnulusGaugeAA: paint the dim track and the bright progress wedge in
one band pass (distance/angle computed once), and use it in the PIN dial.
Net: the dial repaint drops from ~300 ms to ~67 ms, so the arc now fills ~44
frames over a 3 s sweep (was ~10) — a smooth growing bar. Visually identical.
Add a DEBUG_SERIAL_BUTTONS trace at the timing-arc output (elapsed/fill%/digit
per rendered frame) for on-device QA, and native boundary tests asserting the
exact digit slices (e.g. T=3000: 299 ms -> 0, 300 ms -> 1, and the 9 -> 0 wrap).
Close the review-batch coverage gaps with mutation-verified suites
Two new native suites and four extended ones, 59 tests, every one
verified to fail against a deliberately broken source line:
- test_touch_auth_entry_ui (NEW, 29): the touch unlock controller had
zero host coverage while carrying the review's security-critical
behaviors. An amalgam TU (vault-harness precedent) compiles the
device-only controller on the host behind a fake pointer/canvas/board
seam; pins numeric auto-submit, the inert under-length OK key, reveal
auto-hide via the clock hook, secret zeroization on every exit path,
layer transitions, and the zone-button double-fire guard. Adds a
host-side LayoutContext::fromActiveDisplay test seam (device branch
untouched).
- test_pin_policy (NEW, 11): isValidPinLength/clampPinLength (the guard
that keeps PROVISION from creating unlockable-by-nobody vaults) and
clampSecretKind's fail-closed coercion of unknown persisted bytes.
- test_vault_rekey/import/meta/store_facade (+8): rekey carries the
authenticated secretKind forward verbatim, staged-import accessors
fall back to defaults on abort/truncation/no-stage, and the
provisioned kind survives a simulated reboot (invalidate + remount)
for both kinds.
- test_header_component/menu_list/settings_timer_clamp (+11): title-
subtitle leading in headerHeight and baseline placement, value-band
slack boundaries (exact fit / one-glyph overflow / two-line reserve),
and the kTimerNeverMs sentinel vs finite-deadline branches of the
relock policy.
Claude-Session: https://claude.ai/code/session_01XBVa8G5jrkprye4gVAkQSU
Extract the popup input policy so native tests can pin it
The confirm-dialog policy — which physical control fires each modal role
per board class, when the confirm/cancel pair swaps, and which taps a
modal may answer at all — lived as anonymous-namespace state in
popup_system.cpp, reachable only by flashing hardware. The choice-list
half had already been extracted (choiceListNav); this moves the rest.
InputScheme, ModalHints, hintsFor and swapConfirmCancel are now pure
constexpr policy in popup_system.h. The caption tables and the swap
truth table (dial and touch zones always swap so OK confirms, buttons
swap only for tap dialogs, keyboard and free-touch never) are pinned by
native tests instead of being provable only on glass.
The free-touch tap qualification is now a TapQualifier class with the
same shape as the two-button convention's HoldGauge: pure, fed one
pointer frame per poll. Its semantics mirror the device path exactly —
arming requires a frame with no contact AND no pending tap edge (a stale
contact's lift reports both on one frame, and that frame must neither
arm nor resolve), the contact pins to where it began so cross-cell
travel reads as a drag, the edge consumes the contact exactly once so a
non-consuming driver edge cannot replay, and a tap faster than one poll
period is swallowed. The device keeps ONE instance where three hand
rolled latches were; the two consumers (modal cells, choice-list rows)
classify the qualified points and nothing else.
One deliberate asymmetry is preserved and now documented at the type:
single-action alerts acknowledge on the armed LEVEL plus the driver
edge, not on full qualification — any tap that starts after the alert
was drawn acknowledges it, including one faster than a poll period,
while a contact predating the alert still cannot (its edge frame is the
frame arming excludes). That behavior existed; making it a named
accessor (armed()) keeps the next reader from "fixing" it.
Seven new native tests pin the policy and the state machine, including
the artifact cases that motivated it: stale contact never resolves,
origin pinning, edge replay, sub-poll swallow, and mid-contact reset.
Verified on the T-Deck: a confirm modal opens with its Tap cells and a
qualified tap on Cancel resolves and tears it down. 2553 native tests;
all eight variants build.
Claude-Session: https://claude.ai/code/session_01ABhkBJsMKTZAxZh4Vh7jsF
feat(ui): add in-house QR encoder + scalable drawQr component
Self-contained ISO/IEC 18004 byte-mode QR encoder (no vendoring, no deps):
RS ECC over GF(256), block interleaving, function-pattern placement, the 8
mask patterns scored by the 4 penalty rules, and format/version info. Auto
smallest version (1..10, documented RAM cap), ECC L/M/Q/H, malloc-free.
Fix the data-placement bug from the partial work: the zigzag direction was a
running toggle; the correct rule is per-column ((right & 2) == 0) XOR
(col < 6), and the byte-aligned padding / two-copy format-info mapping were
off. The encoder now matches a trusted generator (segno) module-for-module on
pinned known-answer vectors (HELLO WORLD v1-M mask4, a URL v2-M mask2) and
decodes across versions/ECC levels.
Add ui::drawQr (scalable, quiet zone, theme tokens) + ui::qr::wifi escaped
payload builder, and replace the admin WiFi-QR finder-pattern placeholder with
a real scannable QR. Tests: encoder (known-answer + auto-version + caps),
payload escaping, component rendering.
feat(ui): add in-house QR encoder + scalable drawQr component
Self-contained ISO/IEC 18004 byte-mode QR encoder (no vendoring, no deps):
RS ECC over GF(256), block interleaving, function-pattern placement, the 8
mask patterns scored by the 4 penalty rules, and format/version info. Auto
smallest version (1..10, documented RAM cap), ECC L/M/Q/H, malloc-free.
Fix the data-placement bug from the partial work: the zigzag direction was a
running toggle; the correct rule is per-column ((right & 2) == 0) XOR
(col < 6), and the byte-aligned padding / two-copy format-info mapping were
off. The encoder now matches a trusted generator (segno) module-for-module on
pinned known-answer vectors (HELLO WORLD v1-M mask4, a URL v2-M mask2) and
decodes across versions/ECC levels.
Add ui::drawQr (scalable, quiet zone, theme tokens) + ui::qr::wifi escaped
payload builder, and replace the admin WiFi-QR finder-pattern placeholder with
a real scannable QR. Tests: encoder (known-answer + auto-version + caps),
payload escaping, component rendering.
feat(ui): add Phase 2 stateless reference components (ActionBar/Header/BrandMark/StatusChip)
First catalog components on the Canvas + DeviceProfile foundation
(plan/ui-library-modular-reimpl.md Phase 2). Stateless: props in ->
draw via Canvas -> return, reading the orthogonal DeviceProfile for
device adaptation. Purely additive — no existing screen/state/widget/
Canvas changed.
- ActionBar (action_bar_component): affordance-driven footer hints. The
same ActionBarProps render the device-correct presentation picked from
the profile capabilities — 2-button A/B, 3-button A/B/C, CoreInk dial
^/O/v, keyboard key hints, touch bare labels. Mono1/e-paper path uses a
top divider rule + cell inversion instead of the SURFACE fill + ACCENT
hold-fill.
- Header (header_component): title + subtitle + right-packed status
cluster (battery %, BLE, WiFi, lock). Density/orientation-aware via
LayoutContext; Mono1 uses a bottom divider rule, ink-only tokens.
- BrandMark (brand_mark_component): credential brand badge. Reuses the
brands::findBrand/drawBrand registry+atlas on device; falls back to a
deterministic per-domain letter tile (host-testable). Mono1 outlines.
- StatusChip (status_chip_component): self-sizing status pill using
radius.pill (clamped), token color per ChipStyle, knockout/outline text.
Tests (host, RecordingCanvas): 30 new cases across 4 suites, incl. the
per-affordance ActionBar assertions (2btn/3btn/dial/keyboard/touch), the
Mono1 branches, brand letter-fallback + deterministic color, and chip
sizing/geometry. Shared per-variant DeviceProfile factories in
test/support/device_profiles.h.
Verified: fleet 5/5 -Werror (sticks3/gray/cardputer/tdeck/m5core_ink),
native 720/720 (+30), pio check 3 pre-existing HIGH only, all guards +
pre-commit pass.
fix(test): configure clangd and clang-tidy for native test tree (#3)
* fix(test): configure clangd and clang-tidy for native test tree
- Generate compile_commands.json from pio run -e native -t compiledb
so clangd resolves src/ headers in test files without errors.
- Fix .clangd PathMatch regex (relative path, not absolute) and add
-I.pio/build/native/unity_config so unity_config.h resolves.
- Remove overly broad Remove: -I* that was stripping all added paths.
- Add -Wno-format-security and UnusedIncludes: None to test block.
- Add test/.clang-tidy that inherits the root config and:
- Allows test_* functions to use snake_case (FunctionIgnoredRegexp)
- Disables cert-err33-c, cert-dcl50-cpp,
cppcoreguidelines-pro-type-cstyle-cast,
bugprone-argument-comment, bugprone-misplaced-widening-cast,
bugprone-implicit-widening-of-multiplication-result,
misc-unused-using-decls — all false positives in test context.
- Add /* intentionally empty */ comment to all empty setUp()/tearDown()
bodies (28 files) to suppress SonarLint c:S1186.
- Fix test_vault_task: change relative include ../../src/vault/vault_task.h
to vault/vault_task.h (consistent with all other test files; -Isrc
already in compile flags).
- Reorganise test/ into a module-mirroring subtree layout:
test/ble/, test/crypto/, test/hal/, test/ota/, test/platform/,
test/states/, test/totp/, test/ui/, test/vault/, test/web/
(old flat directories removed).
- Add sonar-project.properties exclusion e4: suppress c:S1186 on
test/**/*.cpp at SonarCloud level.
All 468 native unit tests pass (pio test -e native).
* fix(clangd): add -DESP_PLATFORM to global CompileFlags
All Kleidos targets are ESP32/ESP32-S3 (framework = espidf), so
ESP_PLATFORM is always defined at compile time. Without this flag in
the clangd config, the language server was activating #else branches
inside #ifdef ESP_PLATFORM blocks, hiding device code and showing
false diagnostics for src/ files.
The test/** fragment already has Remove: -DESP_PLATFORM, so native
unit-test files continue to compile without the flag.
* revert(clangd): remove incorrect -DESP_PLATFORM from global CompileFlags
Adding -DESP_PLATFORM globally caused 'too many errors' in src/ files
because compile_commands.json was generated from the native env and
lacked all ESP-IDF/Xtensa include paths.
The correct fix is to generate compile_commands.json from a device env:
./scripts/generate_compiledb.sh sticks3
This populates the DB with proper -DESP_PLATFORM and all IDF includes
for every src/ translation unit. The symlink at the project root then
points clangd to the right compilation context.
Close the fleet-UI remainder: severity shape, e-paper grammar, sub-bar contrast
The eight-device review left a tail of findings that needed no owner
ruling. They are landed here, each with the adversarial pass that
followed them — which caught three regressions the first attempt would
have shipped.
Severity stops being carried by color alone (design principle 9, WCAG
1.4.1). A card's mark is now inked in that card's OWN tone rather than a
hardcoded destructive red, so a caution card wears a yellow triangle
instead of a red one whose shape and tint disagree about how bad the
situation is. Transitions deliberately carry no mark at all: the only
shape the dialog component owns means "something is wrong", and the
pending-admin route raises a CAUTION progress card ("Opening admin /
starting WiFi") where a hazard triangle would be a lie. A test pins the
tone on Caution — Danger cannot catch that regression, because the
correct and the hardcoded expression agree there.
CoreInk speaks its own chassis grammar. The credential detail drops to
1-bit because a 6-tone settle blocks the loop ~1.8 s and drops the
button presses a navigable screen depends on; the tab-switch gesture is
advertised; the lock face stops naming a letter that chassis does not
print; the toast body clears the ink floor. The selected tab label lifts
a rung only when it measures inside its cell — the fit reserves headroom
because the canvas measures the regular face while the label draws bold
(1.02-1.05x wider), and where no rung fits at all the string was the
problem: fr "Mot de passe" needed 72px in a 66px cell, so it is now
"MdP", which also fixes a silent overrun on Cardputer.
Indicators that carry information clear the 3:1 non-text bar: empty PIN
slots, page dots and char-wheel positions were on kMuted at 2.16:1 —
information encoded below the floor its own pattern doc prescribed, so
the doc moved too, with the carve-out WCAG grants genuinely inactive
components. The list stops drawing a scroll rail when nothing scrolls.
Documentation that lied is corrected, most consequentially the serial
harness: TAP and HOLD were documented as accepting the same ids as
PRESS, but resolveDefer maps only A/B/C, so TAP P and HOLD K silently
did nothing — a trap for anyone scripting the CoreInk side keys.
Native 2558/2558, seven variants clean.
Claude-Session: https://claude.ai/code/session_01XBVa8G5jrkprye4gVAkQSU
Add the StickS3 UI v2: home pager, detail carousel, TOTP, settings, onboarding
Implements the Claude Design v2 handoff on the StickS3 (135x240), built against
pixel-exact 1:1 mock references extracted from the handoff and verified by
overlay diff against captures from real silicon.
Screens
- Home pager: lock face, timing-arc PIN dial, icon tab strip, vault list.
- Credential detail: 6-stop action carousel (type, username, URL, related TOTP,
notes, delete) over the untouched decrypt/reveal/scrub paths.
- TOTP tab: per-row live codes with countdown rings.
- Settings: navigable tree with Wi-Fi, NTP, timezone and DST screens.
- Onboarding: welcome, button intros, language, five interactive component
tutorials (dial, tabs, lists, values, writing), then PIN creation.
Reusable components (screens compose; components own anatomy)
- CountdownRing, TotpCodeCard, Ipv4Field, TutorialHint, drawPinDialRimDigits and
drawListRowSelectionBand; the inline TOTP ring/code sites now delegate.
Timezone and DST
- Extends the in-house timex engine to 40 zones with POSIX TZ strings and a DST
toggle (default on). The internal epoch stays UTC: timezone and DST convert
display only, so the TOTP epoch (RFC 6238) and the lockout math never shift.
A native test pins that invariance across every zone.
Fixes found on silicon
- popup::confirm/alert blocked the TWDT-subscribed loopTask, so any modal left
open past the 5 s window rebooted the device - reproducible with the
credential delete confirm under no other load. The blocking loops now feed the
watchdog through the platform facade.
- AUDIO ON|OFF never persisted: saveSettings() had no callers repo-wide while
the help text and docs claimed it survived a reboot.
- The onboarding language A-hold confirm could never fire: wasClicked() consumed
the release edge before wasReleaseFor() ran.
- Cancelling the delete confirm exited the whole detail view, via a late release
edge leaking out of the modal.
- Settings entry landed on the second row: multi-tap consumes click edges but
leaves press edges latched, so the first tick read a stale nav.
- The video stream server ran above the vault worker on Core 0 and starved vault
I/O during capture.
QA
- 18 new hardware tests: timezone/DST, TOTP UTC invariance, NTP server
validation, Wi-Fi store, NTP sync, demo seed, audio persistence.
- The video capture test had silently skipped since WEB-05: /stream requires a
token the decoder never sent, so a 401 became a skip. It now sends the token
and fails hard on auth errors.
Native 2258/2258; sticks3, m5stack_gray, m5core_ink and tdeck build -Werror;
clang-tidy reports 0 findings across 269 TUs.
Claude-Session: https://claude.ai/code/session_01P6BNTpbgrvnZXSJnNuj8ZJ
Close the fleet-UI remainder: severity shape, e-paper grammar, sub-bar contrast
The eight-device review left a tail of findings that needed no owner
ruling. They are landed here, each with the adversarial pass that
followed them — which caught three regressions the first attempt would
have shipped.
Severity stops being carried by color alone (design principle 9, WCAG
1.4.1). A card's mark is now inked in that card's OWN tone rather than a
hardcoded destructive red, so a caution card wears a yellow triangle
instead of a red one whose shape and tint disagree about how bad the
situation is. Transitions deliberately carry no mark at all: the only
shape the dialog component owns means "something is wrong", and the
pending-admin route raises a CAUTION progress card ("Opening admin /
starting WiFi") where a hazard triangle would be a lie. A test pins the
tone on Caution — Danger cannot catch that regression, because the
correct and the hardcoded expression agree there.
CoreInk speaks its own chassis grammar. The credential detail drops to
1-bit because a 6-tone settle blocks the loop ~1.8 s and drops the
button presses a navigable screen depends on; the tab-switch gesture is
advertised; the lock face stops naming a letter that chassis does not
print; the toast body clears the ink floor. The selected tab label lifts
a rung only when it measures inside its cell — the fit reserves headroom
because the canvas measures the regular face while the label draws bold
(1.02-1.05x wider), and where no rung fits at all the string was the
problem: fr "Mot de passe" needed 72px in a 66px cell, so it is now
"MdP", which also fixes a silent overrun on Cardputer.
Indicators that carry information clear the 3:1 non-text bar: empty PIN
slots, page dots and char-wheel positions were on kMuted at 2.16:1 —
information encoded below the floor its own pattern doc prescribed, so
the doc moved too, with the carve-out WCAG grants genuinely inactive
components. The list stops drawing a scroll rail when nothing scrolls.
Documentation that lied is corrected, most consequentially the serial
harness: TAP and HOLD were documented as accepting the same ids as
PRESS, but resolveDefer maps only A/B/C, so TAP P and HOLD K silently
did nothing — a trap for anyone scripting the CoreInk side keys.
Native 2558/2558, seven variants clean.
Claude-Session: https://claude.ai/code/session_01XBVa8G5jrkprye4gVAkQSU
Close the review-batch coverage gaps with mutation-verified suites
Two new native suites and four extended ones, 59 tests, every one
verified to fail against a deliberately broken source line:
- test_touch_auth_entry_ui (NEW, 29): the touch unlock controller had
zero host coverage while carrying the review's security-critical
behaviors. An amalgam TU (vault-harness precedent) compiles the
device-only controller on the host behind a fake pointer/canvas/board
seam; pins numeric auto-submit, the inert under-length OK key, reveal
auto-hide via the clock hook, secret zeroization on every exit path,
layer transitions, and the zone-button double-fire guard. Adds a
host-side LayoutContext::fromActiveDisplay test seam (device branch
untouched).
- test_pin_policy (NEW, 11): isValidPinLength/clampPinLength (the guard
that keeps PROVISION from creating unlockable-by-nobody vaults) and
clampSecretKind's fail-closed coercion of unknown persisted bytes.
- test_vault_rekey/import/meta/store_facade (+8): rekey carries the
authenticated secretKind forward verbatim, staged-import accessors
fall back to defaults on abort/truncation/no-stage, and the
provisioned kind survives a simulated reboot (invalidate + remount)
for both kinds.
- test_header_component/menu_list/settings_timer_clamp (+11): title-
subtitle leading in headerHeight and baseline placement, value-band
slack boundaries (exact fit / one-glyph overflow / two-line reserve),
and the kTimerNeverMs sentinel vs finite-deadline branches of the
relock policy.
Claude-Session: https://claude.ai/code/session_01XBVa8G5jrkprye4gVAkQSU
Hold the vault format version and close the design-sheet gaps
Two corrections to the touch keyboard work.
The metadata format version goes back to 0x01. Adding secretKind bumped it to
0x02, but pre-release the only deployment path is a full erase and re-flash, so
no device can be holding the previous layout: the bump bought nothing but a
migration matrix nobody can exercise, and it made the version byte look like it
carried information it does not. The field stays; the byte does not move.
Strict-equality parsing still earns its keep — a foreign or corrupt record fails
closed. This restores agreement with docs/security/vault-encryption.md, which
already documented 0x01 as the sole accepted version.
Then three gaps against the Phase 4 touch sheet, found by re-reading it beside
the implementation:
The PIN rail was drawing one kind of hollow bead. The sheet draws three states —
entered positions solid, the position ABOUT to be entered as a brand-color ring,
the rest dim — and the middle one is the only thing on the screen that says how
far through a fixed-length secret the user is without revealing anything.
Shift and backspace were borrowing ChevronUp and Back. Both already mean
something else in this library: a bare chevron is "more above", and the back
arrow is the navigate-up affordance. On a keyboard those sit a finger's width
from keys that must not share their glyph, so both get their own recipe — a
hollow up-arrow and a pentagon with an X, matching the sheet's key caps.
Also records why the sheet's 34 px title bar stays out and why 3 px key gutters
are not a violation: the header would leave the key rows at 39 px, under the
40 px touch floor, and ten 40 px columns with 8 px gutters need 472 px of a
320 px panel — the letter rows instead satisfy WCAG 2.5.8 through its 24 px
centre-spacing alternative at a 31.5 px pitch, with the gutters live in the hit
test.
Add the StickS3 UI v2: home pager, detail carousel, TOTP, settings, onboarding
Implements the Claude Design v2 handoff on the StickS3 (135x240), built against
pixel-exact 1:1 mock references extracted from the handoff and verified by
overlay diff against captures from real silicon.
Screens
- Home pager: lock face, timing-arc PIN dial, icon tab strip, vault list.
- Credential detail: 6-stop action carousel (type, username, URL, related TOTP,
notes, delete) over the untouched decrypt/reveal/scrub paths.
- TOTP tab: per-row live codes with countdown rings.
- Settings: navigable tree with Wi-Fi, NTP, timezone and DST screens.
- Onboarding: welcome, button intros, language, five interactive component
tutorials (dial, tabs, lists, values, writing), then PIN creation.
Reusable components (screens compose; components own anatomy)
- CountdownRing, TotpCodeCard, Ipv4Field, TutorialHint, drawPinDialRimDigits and
drawListRowSelectionBand; the inline TOTP ring/code sites now delegate.
Timezone and DST
- Extends the in-house timex engine to 40 zones with POSIX TZ strings and a DST
toggle (default on). The internal epoch stays UTC: timezone and DST convert
display only, so the TOTP epoch (RFC 6238) and the lockout math never shift.
A native test pins that invariance across every zone.
Fixes found on silicon
- popup::confirm/alert blocked the TWDT-subscribed loopTask, so any modal left
open past the 5 s window rebooted the device - reproducible with the
credential delete confirm under no other load. The blocking loops now feed the
watchdog through the platform facade.
- AUDIO ON|OFF never persisted: saveSettings() had no callers repo-wide while
the help text and docs claimed it survived a reboot.
- The onboarding language A-hold confirm could never fire: wasClicked() consumed
the release edge before wasReleaseFor() ran.
- Cancelling the delete confirm exited the whole detail view, via a late release
edge leaking out of the modal.
- Settings entry landed on the second row: multi-tap consumes click edges but
leaves press edges latched, so the first tick read a stale nav.
- The video stream server ran above the vault worker on Core 0 and starved vault
I/O during capture.
QA
- 18 new hardware tests: timezone/DST, TOTP UTC invariance, NTP server
validation, Wi-Fi store, NTP sync, demo seed, audio persistence.
- The video capture test had silently skipped since WEB-05: /stream requires a
token the decoder never sent, so a 401 became a skip. It now sends the token
and fails hard on auth errors.
Native 2258/2258; sticks3, m5stack_gray, m5core_ink and tdeck build -Werror;
clang-tidy reports 0 findings across 269 TUs.
Claude-Session: https://claude.ai/code/session_01P6BNTpbgrvnZXSJnNuj8ZJ