Skip to content

Open the web app on a library dashboard (ADR-0058 phase 1) - #171

Merged
jeffcrouse merged 3 commits into
mainfrom
admin/dashboard
Aug 16, 2026
Merged

Open the web app on a library dashboard (ADR-0058 phase 1)#171
jeffcrouse merged 3 commits into
mainfrom
admin/dashboard

Conversation

@jeffcrouse

Copy link
Copy Markdown
Member

Supersedes #168, which GitHub auto-closed when its base branch strip/web-detail-routes was deleted on merging #167. Same commit, rebased onto main.

Proposes ADR-0058: The Web App Is an Administration Tool and ships its phase 1.

The dashboard

The web app opened on Settings — a form, with playback controls above library health, telling the operator its own subject was secondary. It now opens on the state of the library.

Almost none of this is new capability. Both endpoints existed and both already had a client wrapper that nothing called: libraryApi.getStats over /library/stats, playTrackingApi.getStats over /tracks/stats/plays. The four pending-analysis queues are shown separately (point 7) — they are distinct backlogs with distinct version constants, and one number hides which is stuck.

No artwork-coverage tile: there is no endpoint that counts albums without art. Point 6 says the count comes first.

Pointing it at the real library turned it into a backend change

Every total on /library/stats disagreed with the screen it links to:

field stats said list endpoint said why
total_tracks 26,488 26,422 no status == ACTIVE filter — 66 missing/deleted files counted as library size
total_albums 3,873 3,927 count(distinct Track.album) is a string distinct, merging same-titled albums by different artists
total_artists 3,664 3,477 raw tag strings — "The Beatles" and "Beatles, The" counted twice

Each now counts the way the list endpoint counts.

A trap on the way in: scoping the totals to active tracks while leaving the analysis counts unscoped renders a negative backlog — −41 pending on the real library, since 41 analyses belong to tracks that have left it. The analysis counts join Track too, and pending_analysis is clamped.

After deploying, analyzed_tracks fell 26,463 → 26,422 and pending_analysis 25 → 0. The library is fully analysed; that "25 pending" was 66 non-active tracks netted against 41 stale analyses.

What could not be fixed

albums / compilations / soundtracks are meaningless: nothing writes Track.album_type, so every row keeps the column default. The breakdown reads "26,488 albums · 0 compilations" on a library ADR-0052 found 297 compilations in — a consumer with no producer, the mirror of the generated-and-uncalled shape this codebase keeps finding.

They stay on the wire (library is a generated tag under ADR-0007, so dropping required fields breaks the Swift client), deprecated in the model and in types/index.ts, displayed nowhere. docs/REST-API.md had been documenting 150 compilations and 45 soundtracks — values no instance has ever returned. Recorded as an ADR follow-up: either the scanner sets album_type (MusicBrainz release types are already fetched) or the column and the fields go.

Tests

backend/tests/test_library_stats.py asserts stats against the list endpoints, not against constants — "expect 3 albums" passes under either counting method, which is how this drifted unnoticed. All five fail against the previous implementation; verified by stashing the fix.

Verification

  • pytest tests/test_library_stats.py tests/test_api_library.py tests/test_library_artists_canonical.py — 42 passed
  • tsc --noEmit — 14 errors (unchanged baseline)
  • pnpm test — 63 files / 952 tests passed
  • pnpm run build — clean
  • Deployed to the NAS: all three totals now equal the list endpoints exactly (26,422 / 3,927 / 3,477), and the deployed Dashboard chunk contains no compilations reference

ADR-0058 is proposed — points 1–7 need your say-so before phase 2 (navigation).

🤖 Generated with Claude Code

jeffcrouse and others added 3 commits August 16, 2026 17:06
ADR-0058, proposed: the web app is an administration tool, and the first thing an
administrator wants is the state of the thing they administer — not a settings form
with playback controls above library health.

The dashboard itself is an unbuilt front end rather than a new feature. Both endpoints
already existed and both already had a client wrapper nothing called: libraryApi.getStats
over /library/stats, playTrackingApi.getStats over /tracks/stats/plays. The four
pending-analysis queues are shown separately (point 7) because they are distinct backlogs
with distinct version constants, and one number hides which is stuck.

Pointing it at the real 26k library is what turned this into a backend change. Every total
on /library/stats disagreed with the screen it links to:

  total_tracks   26,488 vs 26,422 — no status == ACTIVE filter, so 66 missing/deleted
                 files counted as library size
  total_albums   3,873 vs 3,927 — count(distinct Track.album) is a *string* distinct and
                 merges same-titled albums by different artists; the album list groups by
                 (album_artist, album), case-insensitively
  total_artists  3,664 vs 3,477 — raw tag strings, so "The Beatles" and "Beatles, The"
                 count twice; the artist list reads canonical Artist (ADR-0052)

Each now counts the way the list endpoint counts. Scoping the totals to active tracks
without also scoping the analysis counts would have rendered a *negative* backlog — -41
pending on the real library — so those join Track too, and pending_analysis is clamped.

albums/compilations/soundtracks cannot be fixed: nothing writes Track.album_type, so every
row keeps the column default. That breakdown reads "26,488 albums · 0 compilations" on a
library ADR-0052 found 297 compilations in. A consumer with no producer — the mirror of the
generated-and-uncalled shape this codebase keeps finding. They stay on the wire because
`library` is a generated tag (ADR-0007) and dropping required fields breaks the Swift
client; they are deprecated in the model and the type, and displayed nowhere. REST-API.md
had been documenting 150 compilations and 45 soundtracks, which no instance ever returned.

tests/test_library_stats.py asserts stats against the list endpoints rather than against
constants — "expect 3 albums" passes under either counting method, which is how this drifted
for as long as it did. All five fail against the previous implementation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI's Backend Lint runs `mypy app`, which wanted an annotation for the intermediate
`album_groups` select. Inlining removes the variable rather than annotating it — the
subquery is only ever consumed in the next line.

No behaviour change; tests/test_library_stats.py unchanged and still passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI's "Check committed OpenAPI schema is current" step caught this. The diff is
description-only — the LibraryStats model docstring and the get_library_stats handler
docstring, both rewritten to record why the totals changed. No field, type or path moved.

Clients generate from this file (ADR-0007 phase 2), so a stale one is a client built
against an API that no longer exists — which is why the check exists even for a change
that only alters prose.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jeffcrouse
jeffcrouse merged commit cc561eb into main Aug 16, 2026
1 check passed
@jeffcrouse
jeffcrouse deleted the admin/dashboard branch August 16, 2026 21:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant