fix: admin bugs from the audit: a playback-error delete no longer reports failure, a failed Overview load no longer reads as all-clear, and email settings can't be saved over with defaults (M503 #5604)
release / govulncheck (push) Successful in 16s
release / web (push) Successful in 1m31s
release / go (push) Successful in 1m49s
release / integration (push) Successful in 5m20s
release / Build signed APK (releases and dev) (push) Successful in 5m9s
release / android (push) Successful in 6m26s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 25s
release / Verify release artifacts (tag releases only) (push) Skipped

- Playback errors: Delete file stops resolving the error row the track
  deletion already took with it (ON DELETE CASCADE), which 404'd into
  "Delete failed".
- Overview: a queue that fails to load says so with Try again, and its tile
  shows a dash, not 0. Delete via Lidarr shows only for an album Lidarr
  holds while Lidarr is connected. An approve refreshes the Collection.
- Quarantine: Delete via Lidarr is in the row menu, so a phone reaches it;
  "1 report", not "1 reports".
- Duplicates: the Keep badge follows the copy picked.
- Integrations: provider save failures and load errors are said, not
  swallowed or shown as "No providers registered"; the Lidarr test catches
  a failed call; SMTP fills once and Save stays off until it has loaded.
  Lidarr's blurb says what it is still used for.
- Requests: release profiles point at Collection › Following.
- Library: an empty library sends an admin to Admin to run a scan, and
  tells anyone else the admin scans it, instead of opening Settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 09:40:11 -04:00
co-authored by Claude Opus 5.5
parent c94c09aeae
commit 7c89e59536
14 changed files with 343 additions and 95 deletions
+7 -5
View File
@@ -432,7 +432,8 @@
<ul class="divide-y divide-border">
{#each group.members as m (m.track_id)}
{@const keep = m.track_id === group.survivor_track_id}
{@const keep = keeperOf(group) === m.track_id}
{@const proposed = m.track_id === group.survivor_track_id}
<li class="flex items-start gap-3 px-4 py-3" data-testid="duplicate-member">
<input
type="radio"
@@ -446,14 +447,15 @@
<div class="flex flex-wrap items-center gap-2">
<span class="truncate text-sm text-text-primary">{m.title}</span>
{#if keep}
<!-- The proposed copy to keep, with the rule that chose it.
A default the merge will let the operator override. -->
<!-- The copy the merge will keep: the proposed one with the
rule that chose it, or the one the operator picked. The
badge follows the pick, as the merge does (#5604). -->
<span
class="shrink-0 rounded-full bg-accent-tint px-2 py-0.5 text-xs text-accent-fg"
data-testid="survivor-badge"
title="Proposed to keep: {group.survivor_reason}"
title={proposed ? `Proposed to keep: ${group.survivor_reason}` : 'Your pick'}
>
Keep · {group.survivor_reason}
Keep · {proposed ? group.survivor_reason : 'your pick'}
</span>
{/if}
</div>
@@ -151,6 +151,15 @@ describe('admin duplicates', () => {
expect(rows[1].querySelector('[data-testid="survivor-badge"]')).toBeNull();
});
test('picking another copy moves the keep badge to it', async () => {
renderWith(response());
const rows = screen.getAllByTestId('duplicate-member');
await fireEvent.click(rows[1].querySelector('input[type="radio"]') as HTMLInputElement);
const after = screen.getAllByTestId('duplicate-member');
expect(after[0].querySelector('[data-testid="survivor-badge"]')).toBeNull();
expect(text(after[1].querySelector('[data-testid="survivor-badge"]') as HTMLElement)).toBe('Keep · your pick');
});
test('an acoustic group reads as a match percentage', () => {
const r = response();
r.groups[0] = { ...r.groups[0], tier: 'acoustic', worst_bit_error_rate: 0.04 };