Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. Caps Lock hint "gone".
  2. cargo doc failing in CI on -D warnings.
  3. Docker YAML import "gone".
  4. Settings: expiry-days row, default-account row, experimental project types.
  5. The app shell painting before the lock screen.
  6. 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:

  1. playwright.config.ts - Vite's default bind answers on ::1; baseURL was 127.0.0.1. Failure reads "Timed out waiting 60000ms from config.webServer", which names nothing about addresses.
  2. 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. Now page.evaluate(`(${AUDIT_FN})()`).
  3. 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

  • !important on #sidebar.collapsed, not clearing the inline width in JS. The JS route means two places have to agree about which state owns width; the stylesheet route is one rule with the reason written beside it.
  • visibility, not display, for html.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

  1. The lab's first real findings are in ui-lab/.artifacts/layout-findings.md and are not fixed. The bulk is target-too-small at min-window (860×600) - 16 distinct controls under 24×24, including #add-btn and #vault-switcher at every viewport - plus #search-syntax-hint clipped on both axes. That is Phase 28's actual work; this session only made it runnable.
  2. Confirm the docs workflow goes green on the next push to main, and that Pages is still enabled by hand.
  3. 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.md is gitignored here, so its updates (six bug-history rows, three pitfalls, one decision, the 928→931 count) are local only.
  • npm run ui-lab needs npx playwright install chromium once. Screenshots and .artifacts/ are gitignored; the harness is committed.
  • ui-lab/behaviour.spec.ts is new: it is for interactions whose outcome is the CSS cascade. Put the next "the class should have won" test there, not in tests/.