Phase log 26: 2026-09-04
Six defects reported from using the app after 3a3e3f0/a843ac4, plus the CI
docs failure. Five were real; one was a design trade-off the user wanted back;
one turned out to be a discoverability problem rather than a missing feature.
Fixing them meant running the UI lab for the first time, which had three defects
of its own.
Goal
Close the list the user gave:
- Caps Lock hint "gone".
cargo docfailing in CI on-D warnings.- Docker YAML import "gone".
- Settings: expiry-days row, default-account row, experimental project types.
- The app shell painting before the lock screen.
- The hamburger toggle "weird".
Current Progress
All six answered. Verification:
npx vitest run 931 passed (42 files) # was 928; +3 caps-lock tests
npx tsc --noEmit clean
npm run lint exit 0, 0 errors, 490 warnings (suppressed backlog)
npm run format:check exit 0
cargo check --workspace clean
cargo doc --workspace --no-deps --locked clean
npx playwright test behaviour 3 passed
eslint-suppressions.json lost one no-floating-promises entry via
--prune-suppressions (never --suppress-all), because init() is now
init().catch(...).
The bugs that mattered
Ranked by consequence.
1. The sidebar toggle did nothing - for the users who cared about it
applySidebarLayout() writes a persisted sidebarWidth as an inline style.
An inline declaration outranks any class, so #sidebar.collapsed { width: 0 }
lost. The toggle removed the padding and left the panel at full width. Default
installs (sidebarWidth: 0, no inline style) worked, so this was invisible to
anyone who had never dragged the sidebar - and broken for everyone who had.
Fix: width: 0 !important on the collapsed rule. Verified by reverting: with
a persisted 300px width, collapsed measured 300 before and 1 after.
Five Vitest tests cover this toggle and all five pass either way - jsdom has no
cascade. The regression test therefore lives in ui-lab/behaviour.spec.ts, which
is new and exists for exactly this class of bug.
2. cargo doc failed the docs workflow
error: unresolved link to 'verify' … no item named 'verify' in module 'vault_core'
pub mod totp; carried an outer /// summary. Rustdoc merges that with the
module's own //! block and resolves the combined text in lib.rs's scope,
so every intra-doc link written inside totp.rs broke. warnings = "deny" made
it fatal.
lib.rs already documents this trap, two declarations above, for entropy - the
comment was written after the same failure and the next module did it anyway.
Fix: drop the outer ///.
3. Every launch flashed the vault before the lock screen
#unlock-overlay is display:none until JavaScript adds .open, which cannot
happen until the modules load and vault_exists() answers. Until then the
header, sidebar and an empty "0 secrets" grid were fully painted.
html.booting is written into index.html, so the first paint is already
suppressed. It is cleared by showUnlockModal() and finishInit() - the only
two states boot can end in - and by a new init().catch(), so a boot that throws
shows an error rather than a permanently blank window.
4. Docker was the only project type with no Import in its header
Not missing - misplaced. makeConfigViewHeaderBtns() gives wireguard, nginx,
ssh_config, apache and haproxy an Import button; docker's lived inside the
Services card instead, which is a different place to look and the only header in
the app that does not carry one. Header now has "Import compose" / "Export
compose"; the duplicates inside the card are gone.
5. Number inputs in Settings fought their own spin buttons
s-autolock and s-expiry-days were <input type="number"> with an inline
width:70px. Native spin buttons are drawn inside the box, and 70px minus
22px of padding leaves the value nowhere to go; the digits also sat hard left in
a box pinned hard right. Sizing moved into settings.css (84px, right-aligned,
spinners dropped - both fields have min/max and arrow keys still step), and
the inline widths came out of index.html for all four inputs in the panel.
6. Caps Lock could not speak before the first keystroke
Working as designed, and the design was half the story. The derived reading is right and free but silent until a cased letter is typed - which is exactly the moment a master password goes wrong.
Now every derived reading also scores getModifierState('CapsLock'). After
two agreements the platform is believed for the paths with no character to derive
from: a click into the field, and the Caps Lock key itself. One disagreement
disqualifies it permanently. WebKitGTK's always-true mask fails on the first
lowercase letter anyone types.
There is deliberately no "consistently inverted" state - a stuck mask looks exactly like an inverted one until the lock changes, and believing an inversion is the original bug in mirror image.
Not a bug: experimental project types
Measured end to end in Chromium: toggle → Save Settings → experimentalProjectTypes: true
in localStorage → New Project shows all 11 type buttons. Asked the user to
confirm they pressed Save Settings rather than Escape/Cancel, which restores
the pre-open snapshot by design.
The UI lab had never run, and three things stopped it
Phase 28 says "scaffolded, never run". It was not runnable:
playwright.config.ts- Vite's default bind answers on::1;baseURLwas127.0.0.1. Failure reads "Timed out waiting 60000ms from config.webServer", which names nothing about addresses.page.evaluate(AUDIT_FN)- a string is evaluated as an expression, and the expression is a function literal. The page returned an unserialisable function; every screen died on "findings is not iterable" before its screenshot. Nowpage.evaluate(`(${AUDIT_FN})()`).- The boot never dismissed the onboarding wizard - added in Phase 19, after
the lab was written. It is modal and covers everything the audit measures, so
all 162 tests hit their 30s timeout. Seed
onboardingCompleted: true.
Chromium also had to be installed: npx playwright install chromium.
Six screens were added while in there: the five Settings tabs and the New Project modal. Settings is five panes behind a tab strip and every settings-layout defect reported so far has been a row whose label and control fight for width.
Decisions made
!importanton#sidebar.collapsed, not clearing the inline width in JS. The JS route means two places have to agree about which state ownswidth; the stylesheet route is one rule with the reason written beside it.visibility, notdisplay, forhtml.booting. Layout is settled before the shell appears, so nothing reflows on reveal.- Spin buttons dropped rather than accommodated. At 84px the arrows were
never a usable hit target, and both fields carry
min/max. - Docker's Import moved to the header rather than duplicated. Two Import buttons on one screen is worse than one in the expected place.
- The Caps Lock platform probe needs a quorum of two. One agreement cannot distinguish an honest mask from a stuck one that happens to be right.
What worked
- Reverting to verify. The sidebar fix and the docs fix were both confirmed by putting the bug back and watching the measurement change.
- Measuring in a real browser before changing CSS. The bounding boxes said the Settings inputs were not overflowing the drawer, which killed the obvious wrong fix (widths) and pointed at the spin buttons.
- Asking four questions in one round rather than guessing at "buggy" and "weird". Two of the six reports meant something different from their first reading.
What didn't work
- Guessing from source for visual defects. Half an hour went into reading CSS for the Settings rows before running the lab, which answered it in one screenshot.
- jsdom for anything cascade-shaped. The 931-test suite passes with the sidebar toggle completely broken.
Next steps
- The lab's first real findings are in
ui-lab/.artifacts/layout-findings.mdand are not fixed. The bulk istarget-too-smallat min-window (860×600) - 16 distinct controls under 24×24, including#add-btnand#vault-switcherat every viewport - plus#search-syntax-hintclipped on both axes. That is Phase 28's actual work; this session only made it runnable. - Confirm the docs workflow goes green on the next push to
main, and that Pages is still enabled by hand. - The user should confirm the Caps Lock hint now appears on WebKitGTK: type one letter with Caps Lock on. It will not appear before the first keystroke there, and that is expected - the platform disqualifies itself.
Notes for whoever picks this up
CLAUDE.mdis gitignored here, so its updates (six bug-history rows, three pitfalls, one decision, the 928→931 count) are local only.npm run ui-labneedsnpx playwright install chromiumonce. Screenshots and.artifacts/are gitignored; the harness is committed.ui-lab/behaviour.spec.tsis new: it is for interactions whose outcome is the CSS cascade. Put the next "the class should have won" test there, not intests/.