Remove the M5Stack Gray variant from the fleet
The M5Stack Gray is retired (owner-approved): the fleet drops from 9 to 8
device variants. This removes the Gray as a build target / device while
keeping every reusable driver and HAL layer intact.
Removed (Gray-exclusive wiring only):
- variants/m5stack_gray/ (variant.h, ini, sdkconfig, the four envs)
- src/boards/board_gray.cpp (the DEVICE_ID==5 board-registry factory)
- the web installer mockup (gray.svg + screen png) and app.js entry
- docs/hardware/devices/m5stack-gray.md and every fleet-list / inventory
/ identifier / secure-availability row across docs, CI, and instructions
Kept (per owner: preserve tested drivers/HAL even with no current consumer):
- the IP5306 PMIC driver family (src/drivers/power/ip5306*) — Gray was its
only board; still native-validated by test_small_pmic_regmaps
- the classic-ESP32 RTC_CNTL MMIO regmap (rtc_cntl_esp32_regmap.h) — Gray
was the only classic internal-RTC board; still native-validated
- board_gray_log_wrap.cpp renamed to board_log_printf_wrap.cpp: the
__wrap_log_printf stub is fleet-wide (esp32_base defines the gate), not
Gray-specific
DEVICE_ID 5 is retired and left unused so the remaining ids stay stable.
CI covering set rebalanced (Gray was the classic-ESP32 representative):
sticks3, tdeck, cores3_se, m5core_ink, m5stickc_plus2. m5stickc_plus2
(classic ESP32 + ST7789 full-frame polling + 8 MB OTA) replaces the Gray's
classic-ESP32 + OTA + polling coverage. core2_v13 is intentionally not added
to CI: it is an "Initial" WIP board that currently overflows DRAM at link
(pre-existing), so it cannot be a green gate member.
Native tests 2306/2306. Base envs build green for the new CI set
(sticks3, m5stickc_plus2, tdeck, cores3_se, m5core_ink).
Claude-Session: https://claude.ai/code/session_01P6BNTpbgrvnZXSJnNuj8ZJ
Remove the M5Stack Gray variant from the fleet
The M5Stack Gray is retired (owner-approved): the fleet drops from 9 to 8
device variants. This removes the Gray as a build target / device while
keeping every reusable driver and HAL layer intact.
Removed (Gray-exclusive wiring only):
- variants/m5stack_gray/ (variant.h, ini, sdkconfig, the four envs)
- src/boards/board_gray.cpp (the DEVICE_ID==5 board-registry factory)
- the web installer mockup (gray.svg + screen png) and app.js entry
- docs/hardware/devices/m5stack-gray.md and every fleet-list / inventory
/ identifier / secure-availability row across docs, CI, and instructions
Kept (per owner: preserve tested drivers/HAL even with no current consumer):
- the IP5306 PMIC driver family (src/drivers/power/ip5306*) — Gray was its
only board; still native-validated by test_small_pmic_regmaps
- the classic-ESP32 RTC_CNTL MMIO regmap (rtc_cntl_esp32_regmap.h) — Gray
was the only classic internal-RTC board; still native-validated
- board_gray_log_wrap.cpp renamed to board_log_printf_wrap.cpp: the
__wrap_log_printf stub is fleet-wide (esp32_base defines the gate), not
Gray-specific
DEVICE_ID 5 is retired and left unused so the remaining ids stay stable.
CI covering set rebalanced (Gray was the classic-ESP32 representative):
sticks3, tdeck, cores3_se, m5core_ink, m5stickc_plus2. m5stickc_plus2
(classic ESP32 + ST7789 full-frame polling + 8 MB OTA) replaces the Gray's
classic-ESP32 + OTA + polling coverage. core2_v13 is intentionally not added
to CI: it is an "Initial" WIP board that currently overflows DRAM at link
(pre-existing), so it cannot be a green gate member.
Native tests 2306/2306. Base envs build green for the new CI set
(sticks3, m5stickc_plus2, tdeck, cores3_se, m5core_ink).
Claude-Session: https://claude.ai/code/session_01P6BNTpbgrvnZXSJnNuj8ZJ
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
Remove the M5Stack Gray variant from the fleet
The M5Stack Gray is retired (owner-approved): the fleet drops from 9 to 8
device variants. This removes the Gray as a build target / device while
keeping every reusable driver and HAL layer intact.
Removed (Gray-exclusive wiring only):
- variants/m5stack_gray/ (variant.h, ini, sdkconfig, the four envs)
- src/boards/board_gray.cpp (the DEVICE_ID==5 board-registry factory)
- the web installer mockup (gray.svg + screen png) and app.js entry
- docs/hardware/devices/m5stack-gray.md and every fleet-list / inventory
/ identifier / secure-availability row across docs, CI, and instructions
Kept (per owner: preserve tested drivers/HAL even with no current consumer):
- the IP5306 PMIC driver family (src/drivers/power/ip5306*) — Gray was its
only board; still native-validated by test_small_pmic_regmaps
- the classic-ESP32 RTC_CNTL MMIO regmap (rtc_cntl_esp32_regmap.h) — Gray
was the only classic internal-RTC board; still native-validated
- board_gray_log_wrap.cpp renamed to board_log_printf_wrap.cpp: the
__wrap_log_printf stub is fleet-wide (esp32_base defines the gate), not
Gray-specific
DEVICE_ID 5 is retired and left unused so the remaining ids stay stable.
CI covering set rebalanced (Gray was the classic-ESP32 representative):
sticks3, tdeck, cores3_se, m5core_ink, m5stickc_plus2. m5stickc_plus2
(classic ESP32 + ST7789 full-frame polling + 8 MB OTA) replaces the Gray's
classic-ESP32 + OTA + polling coverage. core2_v13 is intentionally not added
to CI: it is an "Initial" WIP board that currently overflows DRAM at link
(pre-existing), so it cannot be a green gate member.
Native tests 2306/2306. Base envs build green for the new CI set
(sticks3, m5stickc_plus2, tdeck, cores3_se, m5core_ink).
Claude-Session: https://claude.ai/code/session_01P6BNTpbgrvnZXSJnNuj8ZJ
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
Add pytest QA system bootstrap (Phase 0, no hardware)
Introduce the qa/ pytest system that will become the single entry-point for
all Kleidos QA (HIL + a thin wrapper over the native Unity suite), replacing the
~60 loose scripts. Phase 0 lands the no-hardware skeleton:
- pyproject.toml: strict markers/config, live logging, marker taxonomy (layer /
requirement / capability / cost), Python-only ruff+black config.
- requirements.txt + constraints.txt: pinned stack on Python 3.13. Drop
pytest-repeat — its --count collides with pytest-embedded's --count (the
mandated transport foundation); soak uses in-test loops instead.
- qa_lib: config resolver (CLI > env > .env > yaml > default) + MAC-keyed device
and host inventories; central redaction filter (logs + Allure attachments);
per-test redacting logging; transport interfaces + mock DUT/host for the
no-hardware mode; redacting Allure evidence helpers.
- inventory: devices.yaml (all 9 DUTs, derived from docs/device-identifiers.md,
MAC = identity), hosts.yaml (mastodon Linux + Plus1 embedded, coords by
env-var name), .env.example placeholders.
- tests: redaction guard (seeded secrets never leak on log/attachment/plain
paths), config + inventory-drift guard (devices.yaml vs the doc), mock
transport, and the native Unity wrapper (skips cleanly without PlatformIO).
Gate: `pytest -m "not needs_hardware"` green (20 passed, native suite included);
`pytest --collect-only`/`--markers` show the taxonomy; redaction self-test
proves no seeded secret leaks; ruff + black clean.
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
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
Remove the M5Stack Gray variant from the fleet
The M5Stack Gray is retired (owner-approved): the fleet drops from 9 to 8
device variants. This removes the Gray as a build target / device while
keeping every reusable driver and HAL layer intact.
Removed (Gray-exclusive wiring only):
- variants/m5stack_gray/ (variant.h, ini, sdkconfig, the four envs)
- src/boards/board_gray.cpp (the DEVICE_ID==5 board-registry factory)
- the web installer mockup (gray.svg + screen png) and app.js entry
- docs/hardware/devices/m5stack-gray.md and every fleet-list / inventory
/ identifier / secure-availability row across docs, CI, and instructions
Kept (per owner: preserve tested drivers/HAL even with no current consumer):
- the IP5306 PMIC driver family (src/drivers/power/ip5306*) — Gray was its
only board; still native-validated by test_small_pmic_regmaps
- the classic-ESP32 RTC_CNTL MMIO regmap (rtc_cntl_esp32_regmap.h) — Gray
was the only classic internal-RTC board; still native-validated
- board_gray_log_wrap.cpp renamed to board_log_printf_wrap.cpp: the
__wrap_log_printf stub is fleet-wide (esp32_base defines the gate), not
Gray-specific
DEVICE_ID 5 is retired and left unused so the remaining ids stay stable.
CI covering set rebalanced (Gray was the classic-ESP32 representative):
sticks3, tdeck, cores3_se, m5core_ink, m5stickc_plus2. m5stickc_plus2
(classic ESP32 + ST7789 full-frame polling + 8 MB OTA) replaces the Gray's
classic-ESP32 + OTA + polling coverage. core2_v13 is intentionally not added
to CI: it is an "Initial" WIP board that currently overflows DRAM at link
(pre-existing), so it cannot be a green gate member.
Native tests 2306/2306. Base envs build green for the new CI set
(sticks3, m5stickc_plus2, tdeck, cores3_se, m5core_ink).
Claude-Session: https://claude.ai/code/session_01P6BNTpbgrvnZXSJnNuj8ZJ