Skip to content

v2.13.0 — visible failures, real retries, and the wrong-version fix - #56

Merged
rancur merged 2 commits into
mainfrom
v2.13.0-bugs-and-visibility
Aug 10, 2026
Merged

v2.13.0 — visible failures, real retries, and the wrong-version fix#56
rancur merged 2 commits into
mainfrom
v2.13.0-bugs-and-visibility

Conversation

@rancur

@rancur rancur commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Three of the reported feature requests turned out to be different problems than described. Measuring them first is what kept this release small.

Reported Actually
"Missing Tracks page is empty — why does it exist?" A bug. It asked for per_page=500 from an endpoint capped at 200, got a 422, and rendered an empty table.
"Post-processing isn't running" It is. 399/400 recent tracks have cue points; 4 of 5,612 lack them. This was a visibility gap.
"Improve fingerprint mismatch" Not a fingerprint check. It's duration math, and the real fault was upstream in the matcher.

Highlights

api.ts was hiding server errors. It threw away the response body and raised Error("API error: 422"), so every caller's catch { setRows([]) } turned a specific, loud failure into "you have nothing". That is the only reason the Missing Tracks bug survived. The client now preserves status + FastAPI's detail and retries only transport faults and 5xx — never a 4xx, which is deterministic.

The wrong-version fix. The file-index title+artist matcher accepted any file whose title and artist agreed, so extended mixes, radio edits and live takes all matched; verify then rejected them on duration and parked them as errors. Measured across 265 such matches on the live library: 155 within 5s, 110 beyond it, 95 of those by more than 15s. The populations separate cleanly, so matches now require durations to agree within 5s (configurable, fails open when either is unknown).

Bulk retry that isn't inert. already_attempted() treats any prior fallback_attempts row as "we tried this", so resetting the pipeline columns alone produced a track that walked straight back to the same error without re-contacting Soulseek. Bulk retry clears them; the pre-existing single-track retry had the same defect and is fixed too.

One real bug in post-processing: "Auto-Analyze After Sync" silently disabled cue generation, tag lookup and cloud upload as well, despite each having its own checkbox and the toggle's description mentioning only BPM/key.

Also

  • Soulseek in service health, probed by the worker and persisted to app_config (sync-api cannot import worker code)
  • Dashboard month drill-down; the month filter is a half-open range so it can use an index — and tracks had no index at all beyond its implicit PK one
  • The health probe called GET /v1/tracks (Lexicon's entire library) every 10 seconds; now /v1/playlists
  • Bulk operations write one activity row instead of one per track

CI

The repo had ~20 test files and no workflow that ran them. There is one now — pytest (not unittest, which silently collects zero tests from the bare-function suites and reports success), plus a web typecheck and a real next build, which is what catches a useSearchParams() outside a Suspense boundary.

It immediately exposed three latent problems, all fixed here: two test modules fighting over db.py's module-level DB_PATH (decided by whichever imports first), and a Tidal test that had been failing for anyone outside the container because tiddl is installed by the Dockerfile but absent from requirements.txt.

Verification

  • sync-api: 37 passed
  • sync-worker: 279 passed, 0 failed
  • sync-web: typecheck clean, all 11 pages build
  • Duration threshold, error totals and coverage percentages all measured against the live instance

🤖 Generated with Claude Code

https://claude.ai/code/session_01HmUiLHPmKoz215WAWV5eHe

Three reported "feature requests" turned out to be something else. Measuring
first is what kept this release small.

Missing Tracks was never empty, it was erroring: /upload asked for per_page=500
against an endpoint capped at 200. The 422 was invisible because api.ts discarded
the response body, so every caller's `catch { setRows([]) }` turned a specific
server error into "you have no missing tracks". The client now keeps the status
and FastAPI's detail, retries only transport faults and 5xx, and the page pages
through all 270 errors.

"Fingerprint mismatch" involved no fingerprints. 108 of 109 came from the
file-index title+artist matcher, which accepted any file whose title and artist
agreed -- extended mixes, radio edits, live takes. Across 265 such matches on the
live library, 155 land within 5s of the Spotify duration and 110 exceed it (95 by
more than 15s), so the populations separate cleanly at 5s. Renamed Wrong Version.

Post-processing was working all along (399/400 recent tracks have cues; 4 of 5,612
lack them) -- there was just no way to see it. Coverage is now on the dashboard,
including an honest note about what Lexicon's API cannot report. One real bug did
surface: "Auto-Analyze After Sync" was silently disabling cue generation, tag
lookup and cloud upload too.

Bulk retry resolves categories server-side with the same classifier that renders
the count, and clears fallback_attempts -- without which a retry was inert, since
already_attempted() treats any prior row as "we tried this". The single-track
retry had the same defect.

Also: Soulseek health surfaced via the worker (sync-api cannot import worker code);
dashboard month drill-down with a sargable filter and the first real indexes on
tracks; the health probe no longer pulls Lexicon's entire library every 10s; and
CI now actually runs the ~20 test suites that nothing was executing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HmUiLHPmKoz215WAWV5eHe
Comment thread sync-api/tests/test_bulk_retry.py Fixed
tempfile.mktemp returns a name without creating the file, leaving a window in
which another process can create it first (CodeQL py/insecure-temporary-file).
The new test copied the convention used throughout the existing suites, which is
what made CodeQL flag it as a NEW alert on this PR.

Creating a temp directory atomically and owning every path inside it is the same
amount of code. The 19 pre-existing instances elsewhere in the repo are untouched
here -- they are a separate cleanup.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HmUiLHPmKoz215WAWV5eHe
@rancur
rancur merged commit faa014a into main Aug 10, 2026
6 checks passed
@rancur
rancur deleted the v2.13.0-bugs-and-visibility branch August 10, 2026 01:48
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.

2 participants