# UI/UX audit — YellowJacket Date: 2026-08-11. Method: the app driven by hand headlessly (`make dev-headless SEED=default` + `playwright-cli`, then `make dev-headless-fresh` for first run), plus three read-only static reviews. Nothing was changed. - `hands-on.md` (this file) — the empirically confirmed findings, i.e. things observed happening in the running app, with the reproduction. - `a11y.md` — accessibility and interaction model. - `perf.md` — rendering performance, memory, state correctness. - `errors.md` — error handling, empty/loading states, destructive actions. Findings below are numbered `H-n` (hands-on) and cross-reference the static reports where they overlap. The reconciliation plan built from all four files is `.planning/plans/completed/007-ui-reconciliation.md`. --- ## Critical — confirmed by reproduction ### H-1. A keypress on any page silently mutates the Autotag queue Every view the user visits stays mounted forever (`index.ts`, class `view-hidden`), so `disconnectedCallback` never runs and `autotag-view`'s `document` keydown listener (`autotag-view.ts:1188`, handler at `:1706`) stays live for the rest of the session. Reproduced: visited Autotag (Pending 11), navigated to Settings, dispatched `keydown` `s` twice → **Pending 9**. Two albums skipped from a page that was not on screen and gave no feedback. `a` on the same listener is Apply, which rewrites tags on disk. ### H-2. `s` and the arrow keys fire two handlers at once `autotag-view`'s listener and `keyboard-shortcut-service` are both on `document` and neither defers. Reproduced on the Autotag page: pressing `s` emitted `QueueModeChanged` (shuffle toggled) *and* skipped the album. `ArrowUp`/`ArrowDown` navigate the folder list *and* change the volume by 5, so walking the autotag list with the keyboard ramps volume to 0 or 100. ### H-3. The progress bar is a local timer that lies, and a keyboard seek desyncs it by ~30 s `seek-bar.ts:110-116` increments `seekValue` by 1 every 1000 ms and only resyncs when `trackChangeId` changes. Nothing reconciles it against `Player.CurrentPositionSeconds`. Reproduced twice: | | UI | backend | |---|---|---| | steady playback, +10 s | 00:47 → 00:57 | 50 → 60 (constant 3 s lie) | | after 4× `ArrowRight` (seek +5 s) | 00:08 → **00:10** | 11 → **40** | The keyboard seek path (`keyboard-shortcut-service.ts:207-214`) calls `Player.Seek` and never tells the seek bar, so the bar does not move at all — the shortcut looks broken, and the displayed time is wrong for the rest of the track. ### H-4. Every icon in the app is fetched from fontawesome.com at runtime Confirmed from `performance.getEntriesByType('resource')`: `https://ka-f.fontawesome.com/releases/v7.1.0/svgs/solid/house.svg` and 35 more. `setBasePath('/dist/webawesome')` in `index.ts` does not affect the icon resolver, and no `registerIconLibrary` call exists. A desktop music player offline, on a captive portal, or behind a firewall renders **no icons at all**. See `perf.md` M9. ### H-5. The whole app is unusable without a mouse Tabbing through the entire app yields **14 stops**, all of them chrome (library filter, search, one unlabelled track-list button, two queue buttons, five transport buttons, volume, queue toggle, seek). The sidebar nav (`app-sidebar.ts:202`, bare `
  • `), every track row, every album/artist/genre card and every context menu are unreachable. `Enter` on a selected track does nothing — reproduced. The cause of the last part is that `data-shortcut-scope` is **never set anywhere in the codebase**, so `resolveScope` can only return `text-input` or `global`, and the two panel-scoped bindings (`tracklist.play` = Enter, `tracklist.delete` = Delete) are dead shortcuts that the Settings page still advertises as configurable. Related: the closed queue panel is `width: 0` but not `inert` and not `visibility: hidden` (`queue-panel.ts:214`), so its Clear/Add buttons still take tab stops and are read by screen readers — reproduced, they appear in the tab order at x=1440. --- ## Major — confirmed by reproduction ### H-6. Global single-key shortcuts hijack keys from focused controls Defaults (`backend/shortcuts/shortcuts.go:16`) bind unmodified `Space N P S R M / Q ↑ ↓ ← →` at global scope, and the service calls `preventDefault()` on a match. Only text inputs are exempt. So a focused `