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
Polish Allure, add no-hardware layer coverage + docs (Phase 5)
- Drop the unused pytest-embedded dependency. It was never imported — SerialDut
(pyserial) reimplements MAC->port, flashing, expect, and multi-DUT itself (the
plan's documented pyserial fallback). Removing it cuts Allure containers from
~47/test to ~2.6/test and eliminates the record_xml_attribute warning; keep
esptool explicit (bridge-board MAC confirm). Refresh docstrings + constraints.
- Allure: an autouse fixture tags each test with a feature (its layer) + story
(its name) so the dashboard groups by BLE / WiFi / Serial / Video / Smoke /
Native. scripts/allure_report.sh already carries history/trends forward.
- No-hardware layer coverage: tests/video/test_video_decode.py (synthetic KVID
frames: raw + RLE decode, motion score, GIF) and tests/wifi/test_wifi_parse.py,
so every layer has a mock-mode test (32 passed no-hardware).
- Docs: docs/testing/qa-system.md (operator guide — install, run, markers,
Allure, non-negotiables, topology gotchas); CLAUDE.md §2/§4/§11 point QA at qa/.
Gate: pytest -m "not needs_hardware" green with no devices; Allure results carry
per-layer feature labels + evidence; ruff + black clean.
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X