Commit Graph
29 Commits
Author SHA1 Message Date
logan 57fbbdf0d2 feat(ui): a shell a phone can be held in
Build & publish Arch package / arch-package (push) Successful in 2m33s
CI / check (push) Successful in 2m33s
Search index maintenance / maintain-index (push) Successful in 7s
CI / e2e (push) Successful in 5m40s
Plan 016 B2, phase 1. Below 600px the grid drops its sidebar column,
`bottom-nav` becomes the primary navigation, and the shell fits the
viewport instead of scrolling sideways out of it.

600 rather than the sidebar's own 900, because 900 is a laptop and the
answer there is a narrower sidebar, which is still a sidebar. Under 600
there is no room for one at all: 360px of viewport over a 200px nav is
not a layout.

**The tab bar is four destinations and a way to everything else.**
Three to five is where touch targets stop being thumb-sized -- eleven
over 360px is 32px each -- so the four are the ones plan 016's subset
says a phone is for, and "More" opens the *existing* `app-sidebar` in a
drawer rather than listing the destinations a second time. Two lists is
two places to add the next view to.

That reuse has a cost this found the hard way: a shared component
brings its `data-testid`s with it, so rendering the drawer's sidebar
unconditionally put a second `nav-home` (and ten siblings) in the DOM
and **failed 30 existing specs** with "resolved to 2 elements" -- on a
desktop viewport, where this element is `display: none` and the drawer
can never open. It renders only while the drawer is open, and the
component test asserts the absence, because the failure is invisible
from inside the component and lands in files nobody touched.

**What made the shell overflow was minimums, not padding.** Measured at
360px: the body was 652px wide, because a `min-width` in a flex row is
a hard floor and a grid item's implicit minimum is its content. So
`min-width: 0` on the boxes between the viewport and the content, and
each component stands its own non-essential parts down in its *own*
stylesheet -- search-bar's 200px floor, job-indicator's label (the
visible one; the live region that announces it is untouched),
audio-player's seek bar and volume. A media query inside a shadow root
is answered by the viewport, so this is the component saying what it
drops rather than the shell reaching in.

Volume goes because the hardware keys own it on a phone, which is the
same reason mediacontrols' Android handler implements no volume
callback. Seeking goes because 4px is not a thumb target; it belongs to
the full-screen now-playing view, which is the next phase.

An existing spec therefore asserts the opposite of what it did:
layout-overflow's 320px case used to require that the 464px behind
`overflow: hidden` could be *scrolled to*, which was the remedy
available while the shell had one layout. It reflows now -- 320px in a
320px viewport, exactly -- and reflow is what WCAG 1.4.10 asked for.
2026-08-16 23:19:26 -04:00
logan 9d420cda0a fix(a11y): add a skip link, demote the subtitle, and size the sort arrow
a11y.30: `<main id="main-content">` existed and nothing linked to it,
so a keyboard user walked the library filter, the search box, the job
indicator and eleven nav items before reaching content, on every
navigation. Two things in it are load-bearing and only checkable
against the running document: the link is out of flow in *both* states,
because `body` is a grid with named areas and an in-flow extra child is
auto-placed into one of them; and `<main>` needs tabindex="-1", or the
fragment link moves the scroll, leaves the tab sequence where it was,
and looks like it worked.

a11y.29: `<h1>` followed by `<h3>` for type size. An `hgroup` takes one
heading plus paragraphs, so a `<p>` is also what it was meant to hold.

a11y.34: the sort arrow was 10px, below the type scale's own floor,
with a comment acknowledging it. Half of that finding was closed by
Phase 1 — the direction is announced now, via aria-sort — and the other
half is one declaration.

And the state that landed in: the hgroup measured 67px inside a 64px
bar, so dropping the h3's bottom margin shortened the block, moved the
flex-centred pair down, and clipped the subtitle's descenders. The
overflow was pre-existing; `margin-block: 0` on the title is the fix,
pinned by a new layout-overflow case.
2026-08-13 02:09:44 -04:00
logan 1aa1598ecb feat(shortcuts): tell the key story once, and give the arrows back
Build & publish Arch package / arch-package (push) Successful in 2m1s
CI / check (push) Successful in 2m24s
Search index maintenance / maintain-index (push) Successful in 6s
CI / e2e (push) Canceled after 8s
Decision 1 keeps the unmodified single-key bindings, and Settings was
the only place they were written down — three of the four categories of
them, because config-page listed the categories by hand, so the autotag
keys were written down nowhere at all. `?` now opens an overlay from
anywhere the app owns the keyboard, and both surfaces read one table
(services/shortcut-meta.ts, moved out of config-page's private static).

The other half is the same explanation from the other side. Phase 1
gave the arrow keys to the grid, correctly — but all six of them, and
no list in this app moves horizontally: track-list's own handler and
utils/roving-rows both take Up/Down/Home/End and ignore Left/Right. So
seeking stopped working from a focused row and nothing gained the keys.
Reproduced in the running app: two ArrowRights on a focused track row,
zero Player.Seek calls, against one per press from the body.

A shifted character no longer reports Shift, so the binding is `?` and
not `Shift+?` — the character already carries the shift, and a layout
where it does not is a layout where "Shift+?" is wrong anyway.
2026-08-12 12:33:50 -04:00
logan 795f40acee perf(frontend): split the routes and warm the chunks on idle
One 1.18 MB chunk containing all 27 views, every one eagerly imported
and side-effect-evaluated before first paint. `index.ts` now holds a
loader table per view and awaits the right chunk before creating the
element. JS evaluated before first paint: 1 480 kB -> 772.9 kB, in 27
chunks instead of one, with the slowest first open of a view at 19 ms
against 21 ms — both halves of the trade, and the second did not get
worse.

Two things it has to get right. `document.createElement` on an
undefined tag yields an inert HTMLElement rather than throwing, so a
missing entry in the table is a blank page and not an error; and
navigations are numbered, so a slow chunk cannot land on top of a
faster navigation. `notification-host`, `inline-notice` and
`confirm-dialog` stay eager on purpose: a failure surface that has to
fetch a chunk before it can speak is not a failure surface, and the
moment it is most needed is the likeliest moment loading one fails.
2026-08-12 01:19:04 -04:00
logan 5ca6cad45a feat(harness): agent-drivable dev harness and CI that gates
Build & publish Arch package / arch-package (push) Successful in 2m8s
CI / check (push) Failing after 1m56s
CI / e2e (push) Skipped
Search index maintenance / maintain-index (push) Successful in 13s
A coding agent could develop this repo's Go packages and could not
develop the application: every path to running YellowJacket ended in a
blocking GTK window, so 265 bound methods, 46 events, 33 component
directories and 13 stores had exactly one form of verification
available — `tsc --noEmit`.

The unlock is that `wails dev`'s dev server on :34115 serves the real
frontend with the real generated bindings against the same Go backend a
desktop window attaches to, so a plain Chromium under Xvfb gets a fully
functional app. Four test tiers now exist, cheapest first:

- `make ui-test` — 313 Vitest tests in a real browser in ~2 s, no app,
  no backend, no display. Works because `frontend/wailsjs/` is a pure
  passthrough to `window.go`/`window.runtime`, so faking just those two
  globals runs the real bindings and the real store code.
- `make test` — services in-process, asserting on the payload the
  frontend would receive, via a new `events.Emit` wrapper.
- `make dev-headless` + `playwright-cli` — the real app, driven
  interactively, with an event bridge on `window.__yjEvents` and a
  dev-only control surface at `/__test/`.
- `make e2e` — 19 of those flows frozen as Playwright specs.

`events.Emit(ctx, …)` replaces all 35 direct `runtime.EventsEmit` call
sites: wails' `getEvents` `log.Fatalf`s on any context without its
runtime, so those paths could not run under test and a background
worker could take the app down. Four packages had each hand-rolled the
same guard; nine more guarded on `ctx != nil`, which does not help.
`TestNoDirectRuntimeEmits` fails the build on a new one.

Fixtures are generated, not committed (`make testdata`), and seeds are
built by *running the app* — never by hand-writing config and DB rows,
which would be a second description of a valid YJ_HOME.

`.gitea/workflows/ci.yml` is the first workflow here that tests
anything; the other three only package, so `gitea_ci` reported only
packaging jobs and misled anyone asking whether a push was healthy.
Both jobs were prototyped to green in a bare ubuntu:24.04 container
before the YAML was written, which immediately caught `make lint`
linting three configurations that nothing builds: all three passes
omitted `webkit2_41`, so wails resolved webkit2gtk-4.0 — which Arch
still ships and Ubuntu 24.04 dropped.

Operational instructions live in `.pi/skills/yellowjacket-dev/`,
measured discoveries in `.planning/NOTES.md`, and architecture in
`CLAUDE.md` — split by tense, not by topic, because a topical split
gives every new fact two plausible homes. `make skill-check` fails a
commit if the skill cites a make target that does not exist.
2026-08-10 23:20:42 -04:00
yonluandClaude Sonnet 5 e190fd75b9 feat: data lifecycle rewrite, download clients, wanted list, and central catalog index
Build & publish Arch package / arch-package (push) Successful in 2m12s
Search index maintenance / maintain-index (push) Successful in 2h22m28s
Ships the fresh-start schema cleanup: rebuilt explore catalog index
pipeline (dump import, artifact fetch/build, incremental listen-count
refresh), a new download subsystem (Lidarr/Prowlarr/qBittorrent/SABnzbd/
slskd/yt-dlp providers, staging, reconciliation, wanted list), and the
supporting schema/query/store changes across backend and frontend.

Also includes two smaller follow-ups: bump the central index's
rebuild-after cadence from 90 to 180 days, and remove the Explore
"library only" online/offline toggle entirely (frontend-only, no
backend counterpart) rather than carry unused UI/state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2Agd9af5hE7qzti2ackiS
2026-08-06 17:12:01 -04:00
yonluandClaude Opus 5 01bc5f2094 feat(jobs): surface background jobs with progress, logs and controls
Add a central job registry that library scans and search index builds
report into, so background work is visible instead of buried in the
settings page.

- backend/jobs: registry with per-job ring-buffer logs, capability-driven
  controls, and one coalesced JobsChanged snapshot at 4Hz
- pause survives restart via a job_state table; a paused scan is adopted
  back on launch and skipped by the soft scan
- top-bar indicator, popover, details drawer and a Jobs page replacing
  the config page's scan UI; per-library start/stop retained
- scan timing breakdown moves into the job log, Full rescan to the Jobs
  page; delete the orphaned library-manager component

Also add cmd/indexbuild and cmd/indexexport so the explore index can be
built once centrally rather than by every install, which today streams
~205GB from the ListenBrainz spark dump on first run. indexbuild picks
build/refresh/rebuild from index state; the Gitea workflow runs it on
push, weekly, or manually and publishes only when content changed.

fresh-install no longer defaults YJ_HOME under /tmp: it is tmpfs on most
distros, and the import needs ~6GB of real disk.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 14:42:22 -04:00
yonlu 4e52f6b494 fix: icons outside the pill — globe left, track center, hard-drive right
Moved icons to sit outside the toggle track on either side.
Both icons are always fully visible. The active side's icon
gets the accent color, the inactive side dims to secondary.
Track is a minimal 36×20px pill with a sliding thumb.
- Off: globe bright, thumb left, hard-drive dimmed
- On: globe dimmed, thumb right + yellow track, hard-drive accent
2026-03-30 15:58:10 -04:00
yonlu d5a427d885 feat: pill toggle for Library Only — globe ↔ hard-drive icons
Replaced the text button with a sliding pill toggle:
- Left: globe icon (online/explore mode)
- Right: hard-drive icon (library-only mode)
- Thumb slides left↔right with CSS transition
- Inactive: dark background, white thumb on left (globe side)
- Active: accent yellow background, thumb on right (local side)
- Icons dim/brighten based on active state
2026-03-30 15:51:19 -04:00
yonlu d73226b173 feat: Library Only mode — toggle, search, artist page, similar artists
Backend:
- Migration 17: similar_artist_map table stores per-artist similar
  artist relationships (source_mbid → similar_mbid + name + score)
- Tier 4 index build now persists similar artists to this table
- GetLibrarySimilarArtists(mbid) queries similar artists filtered
  by JOIN with the artists table (library-only, no API calls)
- Added db field to explore.Service for direct queries

Frontend:
- ExploreSettingsStore with libraryOnly toggle, persisted to
  localStorage
- Top bar toggle button with active/inactive styling
- Explore search: skips full MB/LB pipeline when library-only,
  uses only searchLibraryCache (pure JS, instant)
- Artist detail page: in library-only mode, skips all API calls
  (no top tracks, no top releases, no LB play count, no MB
  artist lookup). Uses library store for discography, calls
  GetLibrarySimilarArtists for similar artists.
- Similar artists section: changed from horizontal scroll to
  wrapping flex layout with collapsible toggle (Show all N)
- Removed debug artist ranking log
2026-03-30 15:36:37 -04:00
yonlu 42b8cf9f52 feat(13-02): add library filter dropdown and wire all views to respect active filter
- Add selectedLibraryId state + ByLibrary conditional calls to library-store
- Add library filter pass-through methods to library-controller
- Create library-filter dropdown component in top bar
- Wire genre-details, cover-grid, artists-view, genres-view to use ByLibrary queries
- Update album-selection helper to respect library filter
- Regenerate Wails bindings for new ByLibrary methods
- Search remains client-side (automatically filtered by loaded data)
- Playlists remain unfiltered (use separate playlist store)
2026-03-16 09:41:27 -04:00
yonlu 5689c1be42 added genre cache 2026-02-25 10:16:43 -05:00
yonlu 402789e763 search bar 2026-02-20 17:38:29 -05:00
yonlu 88c3736110 added color/ theme settings, new config window 2026-02-20 14:43:35 -05:00
yonlu 481beca00a frontend changes
-smaller thumbnails
-GPU acceleration
-lazy loading/ rendering only visible elements
-cover grid shows album tracks on click
2026-02-16 22:42:13 -05:00
yonlu 3a696c57ac queue panel shares layer with main content, no longer an overlay 2026-02-15 20:29:22 -05:00
logan d78c0584e2 Squash merge audio-player-component into main 2026-02-13 20:39:23 -06:00
logan 99d584783f added basic stylesheets 2025-04-09 17:28:25 -05:00
Logan Jones a0af1230cb removed pico, play button toggles 2025-04-08 19:11:17 -05:00
logan 307aa09640 audio player component loaded 2025-03-28 13:13:41 -05:00
logan 52ac0566fd added pnpm, vite, and typescript support 2025-03-27 21:35:17 -05:00
logan 30683d268d added pnpm, lit, manually load runtime on frontend 2025-03-27 17:43:51 -05:00
logan 8fb37b4807 added first lit component for example 2025-03-24 23:40:04 -05:00
logan b43c479fc0 added makefile, organised FE assets, bump webkit ver for dev 2025-03-23 22:55:31 -05:00
yonlu fe177ef788 starting audio player 2025-03-22 19:40:11 -04:00
logan eba3187f39 saving and displaying the configured library dir 2025-03-22 17:00:21 -05:00
logan 26c02b27ff icons and slight refactor 2025-03-22 02:42:42 -05:00
yonlu 0f5d535a97 added directory picker button 2025-03-20 19:22:27 -04:00
logan abb60cb96a initial commit 2025-03-19 16:45:13 -05:00