feat(library): remove a track from the library without deleting the file

RemoveFromLibrary deletes the audio_files rows the way the scan's own
orphan cleanup does and records each path in excluded_paths. The
exclusion is not an enhancement: without it the next scan finds the
file, sees no row and imports it again, so the button undoes itself.

The soft scan compares files on disk against rows in the database, so
surveyAudioFiles and countAudioFiles both take the exclusion set —
otherwise an excluded path makes the two disagree forever and queues a
full scan on every launch. Deleting a row cascades to queue_tracks, so
the removal calls the same CompactQueue hook RemoveLibrary does.

Also lands the requested badge: library-status-indicator is a button
again where it can act, utils/library-status.ts states once what owning
and wanting mean, and the long-declared queued state finally has a
producer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDCbcCZQepnpSQYJ6SxxZm
This commit is contained in:
2026-08-14 13:12:01 -04:00
co-authored by Claude Opus 5
parent dcc40b1781
commit dc890d1fcc
48 changed files with 3450 additions and 103 deletions
+16 -6
View File
@@ -18,7 +18,7 @@ here has disappeared.
## Read this part before you fail
Fourteen things cost a cycle each the first time. They are here, not in a
Fifteen things cost a cycle each the first time. They are here, not in a
reference, because you need them *before* the failure, not after.
- **Time out every binding call.** A bound Go method called with wrong
@@ -54,6 +54,16 @@ reference, because you need them *before* the failure, not after.
`label` to `aria-labelledby` — so a new dialog that forgets to call
the helper from `updated()` is invisible to
`getByRole('dialog', {name})`.
- **A name is computed on the element carrying the *role*, and Web
Awesome puts the role in its own shadow root.** `aria-label` on a
`<wa-slider>` or a `<wa-dialog>` host never reaches the tree. Use the
component's own `label` (plus `styles/wa-slider-label.css.ts`, since
a slider's is visible) or `utils/name-dialog.ts`. And in the light
DOM, a `<label>` that is a *sibling* of its control with no `for`
names nothing — that was 24 of the 93 controls on Settings.
**`getFullAXTree` is how you check, and "0 unnamed" is not the whole
answer**: a `placeholder` is an accname fallback, so a box labelled
only by one reports clean.
- **The a11y snapshot cannot check an accessible name on a dialog.**
`playwright-cli snapshot` prints `- dialog [ref=…]` with no name
whether the dialog is named by `aria-labelledby`, by `aria-label`,
@@ -66,7 +76,7 @@ reference, because you need them *before* the failure, not after.
`--browser=webkit` is CI-only; local work is Chromium. CI runs it
with `if: !cancelled()` so a chromium failure does not silently
skip it, which it did for two sessions.
- **CI's `e2e` job is green on both engines** (54 specs each) since the
- **CI's `e2e` job is green on both engines** (88 specs each) since the
container got an audio device that keeps time. If playback specs
start failing there again, check the **`The sink plays at real time`**
step first: ALSA's `null` plugin consumes 3000 ms of audio in 2.96 ms,
@@ -74,9 +84,9 @@ reference, because you need them *before* the failure, not after.
reads as an app bug and cost two sessions of that suspicion.
- **`make e2e` needs `SEED=default`.** Its specs assert on fixture
content — unicode tracks, the fixture artists, a known playable file.
Run against the `bulk` seed a measurement session left behind and 13
of 36 fail, in a list that reads exactly like a regression in
whatever you are holding. `make dev-headless SEED=default` first.
Run against the `bulk` seed a measurement session left behind and a
third of them fail (13 of 36, when it was measured), in a list that
reads exactly like a regression in whatever you are holding. `make dev-headless SEED=default` first.
- **…and the suite spends state it cannot always give back.**
`view-lifecycle.spec.ts` **skips an autotag album** on every run, out
of the eleven the seed has, and does not put it back — so around the
@@ -180,7 +190,7 @@ and a CI step over every commit in a push) rejects a subject that is not
`--no-verify` commit skips it locally and meets it in CI.
Two things about the e2e tier that are not obvious until they bite.
**The 36 specs share one backend process in file order**, so a spec
**The 88 specs share one backend process in file order**, so a spec
that leaves the app somewhere passes alone and fails the suite — leave
the UI as you found it, and *wait* for it rather than trusting the
click to have finished. The queue panel's width is animated and the
@@ -5,7 +5,21 @@ chain got wrong, and when squashing is legitimate — is in `CLAUDE.md`
under *Backend packages → database*. Read it once. This is the
checklist.
A schema change needs **two** files, not one:
**A brand-new table needs one file, not two.** The rule below is about
a *column added to a table that already exists*. `applySchema` runs
every file in `sql/schemas/` on every open, so a
`CREATE TABLE IF NOT EXISTS` reaches an existing install verbatim and a
migration for it would be a second description of the same table — the
thing the third rule forbids. Its indexes go in the schema file too,
because the column and the index arrive together.
A new table has a second gate: **`backend/datamap`**. Add an entry
stating its Kind and Lifetime, or `TestCatalogCoversSchema` fails — and
if it is `Authored` and cascades, `TestAuthoredCascadesAreDeliberate`
wants an explicit exemption with a note, because authored data is what
a user cannot get back.
Adding a **column** to an existing table needs **two** files, not one:
1. **`backend/database/sql/schemas/*.sql`** — `CREATE TABLE ... IF NOT
EXISTS`, the literal target shape, what sqlc reads and what a fresh