# 007 — UI reconciliation: lifecycle, truth, voice, scale, shape, and one thing that was never built **Status:** implemented — all six phases shipped. **Branch:** main **Created:** 2026-08-11 **Follows:** 006-orientation-fixes **Followed by:** 008-the-last-audit **Source:** `.planning/audits/2026-08-11-ui/` — `hands-on.md` (24 findings reproduced in the running app), `a11y.md` (34), `perf.md` (30), `errors.md` (30). ## Recap ~118 findings, which were five problems each spread by copying. All six phases shipped, over eleven passes, and three of the four source audits are closed: `hands-on.md`, `perf.md` and `errors.md` have nothing open. `a11y.md` does, and is plan 008. - **Phase 1 — a cached view now has a lifecycle.** `viewActivated` / `viewDeactivated`, the ambient shortcut scope the mechanism was built for and had never had a caller, and keyboard reach for the sidebar, the track rows, the card grids and the closed queue panel. - **Phase 2 — the player reports its own position.** 1 Hz from the backend, `PlaybackFailed` from both failure paths, auto-advance that skips, and a bar that no longer counts itself 30 s adrift. - **Phase 3 — one notification surface, four levels**, replacing 84 `catch` blocks that ended at `console.error` and two private toasts. - **Phase 4 — works offline and at 50 000 tracks**, over six passes: bundled icons, route splitting, the `TrackPlayCountChanged` split, bounded caches, `utils/track-index.ts` (3–6 s → 68 ms), and the virtualizer repaint rule. - **Phase 5 — one app, not eleven pages**, over four passes: one ``, a measured window minimum, every dialog a named `wa-dialog`, one menu keyboard model, the `?` overlay, and an album page with a primary action that means the same thing in three different ownership states. - **Phase 6 — Explore opens with shelves**, plus the two inherited one-liners: the badge that was an inert button, and the card grids that moved to the end rather than by a row. **What it is worth reading for**: the seven "where the plan was wrong" lists below, seventy-nine entries across the passes. About a third are the audit being wrong rather than the code, and they are the reason plan 008 treats every remaining `a11y.md` claim as a hypothesis. **Inherited, unfinished, and carried into 008**: `tracklist.delete`, which survived all six phases because it needs a "remove from library" operation that does not exist and a decision about what it removes. ## Problem A full pass over the UI — the app driven by hand headless, plus three read-only static reviews — turned up ~118 findings. They are not 118 problems. They are **five**, each of which has spread by being copied rather than fixed: 1. **A view that is off-screen is still running.** `index.ts:74` caches primary views and hides them with a class, deliberately, so `scrollTop` survives navigation. Nothing else was told. So `disconnectedCallback` never fires for a cached view, and everything that was written to clean up there — document listeners, intervals, subscriptions — never cleans up. The worst case is not a leak: it is that pressing `s` on the **Settings** page skipped two albums out of the Autotag queue, because `autotag-view`'s document keydown handler is still live. `a` on that same handler rewrites tags on disk. 2. **The player does not report what it is doing.** The seek bar is a `setInterval` counter that increments by 1/second and reconciles with the backend only on track change. Measured 3 s behind during steady playback; **30 s behind after four keyboard seeks**, because the seek shortcut never tells the bar. A track that fails to load is a silent no-op — `queue.go:1181` logs and returns `false`, the bindings return `void`, and nothing is emitted. `SeekFailed` *is* emitted and has no listener. 3. **Failure has no voice.** There is no app-level notification surface. Two components grew private toasts; the other 84 `catch` blocks end at `console.error`. A user with a moved file, a locked database or an offline network sees a button that does nothing. Where errors *do* surface, eight sites print the raw Go string. 4. **Nothing was built for a large library or a closed network.** Every `` is fetched from `ka-f.fontawesome.com` at runtime, so the app has no icons offline. Finishing a track invalidates and refetches the entire library because `TrackMetadataChanged` means both "tags rewritten" and "play count +1". Two caches are unbounded. The bundle is one 1.18 MB chunk containing all 27 views. 5. **The shape of the app disagrees with itself.** Four views have a page heading and four do not. Two have sort controls. The header search looks global and is view-scoped — searching "tide" on Playlists reports "No playlists match your search" with three *Tideline* tracks in the library. The track list is 40 px wider than its container by arithmetic, so the last column is always clipped. The app never lands on Home. An album page has no way to play the album. Cutting across all five: **the app cannot be used without a mouse.** Fourteen tab stops exist, all of them chrome. ## Ordering principle Not by severity — by *blast radius, then by dependency*. Phase 1 first because it is the only finding that loses user data, and because the lifecycle fix is a precondition for a third of everything else (the leaks, the wasted renders, the double-fired shortcuts). Phase 2 next because the seek bar and silent playback failure are the same surface — the one the user looks at most — and both are lies about state the backend already knows. Phase 3 third because it is the enabling work for a long tail: ~30 findings are "the failure is invisible", and they cannot be fixed one at a time until there is somewhere to put a message. Phase 4 and 5 are independent of each other and of 1–3. Phase 6 is last and is the odd one out: it is the only phase that adds something rather than fixing something. It sits here rather than in its own plan because it depends on Phase 3's notification levels and Phase 5's page header, and because the audit found the Explore page's emptiness to be a UX failure of the same kind as the rest — the app knowing something and not saying it. Each phase is independently shippable and independently verifiable. None of them is a refactor of something that works. --- ## Phase 1 — A view that is not on screen is not listening ### What's wrong `H-1`, `H-2`, `H-5`, `H-6`, and the root cause behind `perf.m3`, `perf.M1`, `perf.M6`, `perf.p6`. - Cached views never deactivate. `autotag-view`'s document keydown fires from every other page; `downloads-view`'s 30 s interval ticks forever; `config-page` re-renders every 3 s for the session. - Two document keydown listeners fire for the same key with no arbitration: on Autotag, `s` skips the album *and* toggles shuffle; `↑`/`↓` move the folder selection *and* change the volume by 5. - `data-shortcut-scope` is read by `keyboard-shortcut-service.ts:147` and **set nowhere in the codebase**, so `resolveScope` can only return `text-input` or `global`, and the two panel-scoped bindings (Enter = play, Delete = delete) are dead — while Settings advertises them as configurable. - Global bindings are unmodified `Space N P S R M / Q ↑ ↓ ← →` with `preventDefault()`, so a focused button cannot be activated with Space and a `