feat: the admin settings cards read as menus: each a titled group of rows with one short line, status first, failures in words (M503 #5607)
release / govulncheck (push) Successful in 16s
release / web (push) Failing after 1m27s
release / go (push) Successful in 1m44s
release / android (push) Successful in 5m13s
release / integration (push) Successful in 5m18s
release / Build signed APK (releases and dev) (push) Successful in 5m26s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Skipped
release / Verify release artifacts (tag releases only) (push) Skipped

Indexers and clients, searching, missing files, Lidarr's neighbours, AcoustID, fingerprinting, loudness, outgoing email's schedule, public address and proxies move from paragraph cards to SettingsGroup and SettingRow. Switches are role="switch". Also fixes the two type errors run 9023 found: the Lidarr test result is narrowed before its lists are read, and the tag-source save mock carries version_bumped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 11:25:46 -04:00
co-authored by Claude Opus 5.5
parent 7e1cfb8d08
commit 7f913e3e70
16 changed files with 449 additions and 916 deletions
@@ -1,6 +1,5 @@
<script lang="ts">
import { onMount } from 'svelte';
import { TriangleAlert } from 'lucide-svelte';
import {
getReacquisitionSettings,
updateReacquisitionSettings,
@@ -8,6 +7,8 @@
} from '#lib/api/admin.js';
import { errMessage } from '#lib/api/errors.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
import SettingsGroup from '#lib/components/settings/SettingsGroup.svelte';
import SettingRow from '#lib/components/settings/SettingRow.svelte';
// Policy for putting an album whose files went missing back on the wanted
// list (milestone #290, M502 #5520). On Admin › Acquisition with the rest
@@ -15,6 +16,10 @@
// links here. Searching for a want, and retrying, is the search
// schedule's job, so there is no retry policy here.
const button =
'rounded-md border border-border px-3 py-1.5 text-sm text-text-secondary hover:bg-surface-hover hover:text-text-primary disabled:opacity-50';
const field = 'w-24 rounded-md border border-border bg-background px-2 py-1.5 text-sm';
let saved = $state<ReacquisitionSettings | null>(null);
let form = $state<ReacquisitionSettings | null>(null);
let saving = $state(false);
@@ -69,97 +74,37 @@
</script>
<section class="space-y-4 rounded-xl border border-border bg-surface p-5">
<div>
<h3 class="font-display text-lg font-medium text-text-primary">Automatic re-acquisition</h3>
<p class="mt-1 text-sm text-text-secondary">
When a file has been missing for a while, Minstrel can put its album back on the
<a href="/collection/missing" class="text-accent-fg hover:underline">wanted list</a>
by itself. It's wanted per album, not per track: a whole folder going missing is one
want, not forty.
</p>
</div>
<SettingsGroup title="Missing files" id="reacquisition" hint="Put an album back on the wanted list when its files go missing.">
{#if loadError}
<p class="text-sm text-action-destructive">
<p class="px-4 py-3 text-sm text-error-fg">
Couldn't load re-acquisition settings.
<button type="button" class="underline hover:no-underline" onclick={load}>Try again</button>
</p>
{:else if form === null}
<p class="text-sm text-text-secondary">Loading…</p>
<p class="px-4 py-3 text-sm text-text-secondary">Loading…</p>
{:else}
<label class="flex items-start gap-3">
<input type="checkbox" bind:checked={form.enabled} class="mt-1" />
<span>
<span class="text-sm text-text-primary">Want missing albums back automatically</span>
<span class="block text-xs text-text-secondary">
Off leaves missing files listed here and does nothing about them.
</span>
</span>
</label>
<div class="grid gap-4 sm:grid-cols-2">
<label class="block">
<span class="text-sm text-text-primary">Wait before wanting it back</span>
<span class="block text-xs text-text-secondary">
Hours a file must be missing before its album is wanted. A drive that
didn't mount, or a container that started before its media, resolves
itself well inside a day; this is what stops those becoming wants.
</span>
<input
type="number"
min="1"
max="720"
bind:value={form.grace_hours}
class="mt-1 w-28 rounded border border-border bg-background px-2 py-1 text-sm
text-text-primary focus-visible:outline-solid focus-visible:outline-2
focus-visible:outline-accent"
/>
</label>
<label class="block">
<span class="text-sm text-text-primary">Albums per sweep</span>
<span class="block text-xs text-text-secondary">
Ceiling on albums wanted each hour, so a large loss trickles.
</span>
<input
type="number"
min="1"
max="200"
bind:value={form.max_per_pass}
class="mt-1 w-28 rounded border border-border bg-background px-2 py-1 text-sm
text-text-primary focus-visible:outline-solid focus-visible:outline-2
focus-visible:outline-accent"
/>
</label>
</div>
<SettingRow label="Want missing albums back" for="reacq-enabled" hint="Per album, not per track: a lost folder is one want.">
<input id="reacq-enabled" type="checkbox" role="switch" aria-checked={form.enabled} bind:checked={form.enabled} class="h-4 w-4 cursor-pointer accent-accent" />
</SettingRow>
<SettingRow label="Wait before wanting it back" for="reacq-grace" hint="Rides out a drive that didn't mount in time.">
<input id="reacq-grace" type="number" min="1" max="720" bind:value={form.grace_hours} class={field} />
<span class="text-sm text-text-secondary">hours</span>
</SettingRow>
<SettingRow label="Albums per sweep" for="reacq-max" hint="Each hour, so a large loss trickles in.">
<input id="reacq-max" type="number" min="1" max="200" bind:value={form.max_per_pass} class={field} />
</SettingRow>
{#if saved && saved.unnameable_albums > 0}
<!-- Stated rather than left to be inferred: without this, an operator
watching those rows never get wanted would reasonably conclude
the feature is broken. -->
<p class="flex items-start gap-2 rounded-md bg-surface-hover px-3 py-2 text-xs text-text-secondary">
<TriangleAlert size={14} class="mt-0.5 shrink-0 text-action-destructive" aria-hidden="true" />
<span>
{saved.unnameable_albums}
{saved.unnameable_albums === 1 ? 'album has' : 'albums have'} missing files but no
MusicBrainz ID, so {saved.unnameable_albums === 1 ? 'it' : 'they'} can't be wanted:
there's no way to identify the release. A re-scan after tagging fixes it.
</span>
<!-- Said outright: otherwise rows that never get wanted read as a broken feature. -->
<p class="px-4 py-3 text-sm text-text-primary">
{saved.unnameable_albums}
{saved.unnameable_albums === 1 ? 'album has' : 'albums have'} missing files but no MusicBrainz ID,
so {saved.unnameable_albums === 1 ? 'it' : 'they'} can't be wanted. Tag and re-scan to fix.
</p>
{/if}
<div class="flex justify-end">
<button
type="button"
class="rounded-md bg-action-secondary px-4 py-2 text-sm text-action-fg hover:opacity-90
focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent
disabled:cursor-not-allowed disabled:opacity-50"
disabled={!dirty || saving}
onclick={save}
>
<div class="flex justify-end px-4 py-3">
<button type="button" class={button} disabled={!dirty || saving} onclick={save}>
{saving ? 'Saving…' : 'Save'}
</button>
</div>
{/if}
</section>
</SettingsGroup>