Commit Graph
5 Commits
Author SHA1 Message Date
yonluandClaude Opus 5 edb13a6f39 perf(explore): ask the disk once, prefetch once, and menu the releases
Three things on the Explore surfaces, all about not asking twice.

A portrait already on disk costs no network call. explore-view seeded
only from the library store — owned artists, which on a catalog search
is nearly none of the results — and sent everything else to
GetArtistImageURL, the resolving entry point, one await at a time.
GetArtistImagesCachedPaths asks the disk about every unresolved artist
in one call, and only what it does not answer reaches the resolver,
in parallel.

The artist page's two sections both wanted PrefetchReleases and each
called it, so the most expensive call the app makes was issued twice
for an overlapping set on a 1 req/s limiter. They are collected and
sent once on a microtask, and prefetchRequested stops the cold-artist
refetch re-asking for what it already asked for.

The release cards — most of the artist page — had no context menu at
all. They have one now on both release shapes, normalised to a
ReleaseMenuTarget when the menu opens so the union does not reach the
action handlers. It is a discriminated union rather than one nullable
field per kind because the panel is shared with the track menu: that is
what keeps aria-label moving with the target, which is the fault
cover-grid shipped. Which items appear is three different questions —
playback is gated on a local album id, not on "owned", and the request
needs a catalog MBID, so it is absent for a library-only release.

Note on the docs: the CLAUDE.md and NOTES.md prose here was
reconstructed after a mishandled `git stash --keep-index` destroyed the
uncommitted originals. One NOTES.md section is marked as incomplete
where its text could not be recovered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDCbcCZQepnpSQYJ6SxxZm
2026-08-14 13:34:15 -04:00
logan 49b1194333 fix(a11y): give the semantic colours a ramp, and every fill a foreground
The contrast pass found two things larger than itself, both recorded as
not-fixed. This is them.

The semantic colours were 'fixed across themes', and one fixed colour
cannot clear 4.5:1 against both a near-black and a near-white surface:
--yj-error measured 2.55:1 on dark's elevated, --yj-info 2.31:1, and
success and warning failed on dark and light both. They are split by the
question they answer. A *fill* is 'what colour is a danger button' --
red in every theme, unchanged -- and a *text* colour is 'what colour is
the word failed on this background', which is now per ramp.

Every fill also carries a computed foreground. White on the default
accent is 1.43:1, and the accent is a colour picker, so no fixed answer
survives it: --yj-accent-fg and the four semantic -fg values are derived
(white if white clears, else black), which keeps a red danger button
white and flips a green or amber one to black. Two accent buttons took
their foreground from --yj-bg-base, which inverts with the ramp -- that
is exactly the white-on-yellow 'Apply (A)' the light theme showed.

Accent used as text gets the same treatment through accentTextOn(),
which mixes along the hue until it clears the ramp's surface and stops.
On both dark ramps it returns the accent unchanged, so the dark themes
are visually untouched by that half.

Measured across three ramps and twelve views: 2237 nodes, 0 failing,
against 110 on dark and 50 on light before. Borders, outlines and
shadows were explicitly kept on the fill token -- a border is not text,
and the first pass of the rewrite moved 30 of them by accident.
2026-08-13 01:05:37 -04:00
logan 533c084f8a fix(a11y): make every text colour clear WCAG AA on every ramp
a11y.md flagged --yj-text-tertiary on --yj-bg-surface as 'borderline
(~4.1:1) but that needs a real measurement', and plan 007 parked it as
'worth measuring before planning'. Measured, against the rendered app
and then across all three background ramps: it failed AA in nine of
twelve text/surface combinations, as low as 2.31:1 on dark's overlay
and 2.55:1 on light's -- the app's most-used secondary text colour,
failing on every view. Not borderline. 110 failing nodes across twelve
views, now 0 of 659.

Three separate mechanisms, and only the first is the finding:

- The ramps. Tertiary is raised per ramp (#a6a6a6 dark, #949494 darker,
  #5c636a light), sized to the lightest surface it actually sits on and
  keeping its hue. Sizing it to bgOverlay too would need a grey lighter
  than secondary, so bgOverlay is documented as not a text surface and
  the one component that put text there uses primary.
- The avatar generator. hsl(hue, 45%, 35%) behind white initials failed
  for 35 of the 360 hues -- the yellow-green band -- so which artists
  were unreadable depended on how their names hashed. The two a sweep
  found were not the finding. 32% clears every hue.
- Jobs' local #ff6b6b, at 4.15:1 on elevated.

Pinned by a unit test over the palette table rather than a DOM sweep:
the ramps are pure data, and checking only what happens to be on screen
is exactly how the light ramp went unexamined. Note that make ui-visual
cannot see any of this -- the component tier renders the fallbacks,
because theme-store sets :root only in the real app.
2026-08-13 00:33:50 -04:00
logan 2518385330 refactor(frontend): adopt the lifecycle and the notification surface
The remaining views, brought onto the two mechanisms added earlier in
this series.

The lifecycle: every cached primary view moves its document listeners,
intervals and event subscriptions off connect/disconnect and onto
`viewActivated`/`viewDeactivated`, so `autotag-view` stops fielding
keystrokes from Settings, `downloads-view`'s 30 s clock stops ticking
for the session, and an off-screen view stops rendering on every
search keystroke. `autotag-view` keeps a document listener only for
Escape, whose dialogs Phase 5 migrates to wa-dialog anyway.

The voice: the silent failures now speak — scan and full rescan (with
a guard against the double-click the coalescing window allowed), job
pause/resume/cancel, playlist delete, download request pause/remove/
clear, add and rename library, add-to-playlist, playlist track
removal, autotag's dialogs and its apply, and favourite reverts. Both
private toasts are gone, along with their CSS and keyframes. Playlist
delete (single and the multi-select loop), download-request removal,
download-client removal and a queue clear over 20 tracks ask first.

Loading, empty and failed become three states rather than one, in
`track-list` and `genre-details` — the first is on the first screen a
new user ever sees — and the Settings index panel seeds itself with
`GetIndexStatus()` instead of waiting forever for a change event.
`smart-playlist-editor` and `download-picker` take the request-version
guard `explore-view` already had.

`track-details` loads through one memoised dynamic import in all ten
openers, which is what takes its 42 kB out of the startup chunk: an
un-upgraded custom element is a real HTMLElement on which `?.show()`
throws, so each opener awaits it before touching the element its
template already rendered.
2026-08-12 01:19:47 -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