Background long-running actions; confirm destructive ones

- Run now (jobs and playlists), dedup scan, genre preview/apply, and manual
  import now dispatch via BackgroundTasks and redirect immediately, instead
  of awaiting a job that can run for the better part of an hour and hang the
  browser or reverse proxy. Progress shows in the Jobs runs table (which
  already polls); if the pipeline is busy the run records skipped_lock there.
- Fix the import banner, which claimed work was ongoing after the request
  had actually blocked to completion; it now reflects the backgrounded start.
- Add confirmation prompts to the dedup per-row and bulk delete and to
  "Fix genres now", matching the existing confirms on library and playlist
  deletes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
andrew
2026-07-09 14:31:54 -06:00
parent b135f11557
commit 8f1fc458f6
8 changed files with 61 additions and 43 deletions
+6 -4
View File
@@ -13,13 +13,14 @@
</div>
</div>
{% if request.query_params.get('skipped') %}
<div class="notice warning">One or both passes didn't run — something else was using the pipeline at that moment (check <a href="/settings/jobs">Jobs</a> for what). Try again in a moment.</div>
{% if request.query_params.get('started') %}
<div class="notice">Scan started. It runs in the background; reload this page in a bit to see new candidates, or watch its progress under <a href="/settings/jobs">Jobs</a>.</div>
{% endif %}
<h2>Pending candidates</h2>
<p class="muted" style="margin-top:-0.5rem;">Paths are relative to the library root. Hover a name for the full path.</p>
<form id="bulk-delete-form" method="post" action="/dedup/confirm"></form>
<form id="bulk-delete-form" method="post" action="/dedup/confirm"
onsubmit="return confirm('Permanently delete every selected file from disk? This cannot be undone.')"></form>
<div class="table-wrap scroll">
<table class="dedup-table">
<colgroup>
@@ -39,7 +40,8 @@
<td class="mono path-cell" title="{{ c.delete_path }}">{{ c.delete_display }}</td>
<td class="muted">{{ (c.delete_size_bytes / 1024 / 1024) | round(1) if c.delete_size_bytes else '?' }} MB</td>
<td class="actions-cell">
<form method="post" action="/dedup/confirm" class="inline">
<form method="post" action="/dedup/confirm" class="inline"
onsubmit="return confirm('Permanently delete this file from disk?\n{{ c.delete_display }}')">
<input type="hidden" name="candidate_id" value="{{ c.id }}">
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
</form>
+4 -3
View File
@@ -8,8 +8,8 @@
</div>
</div>
{% if request.query_params.get('skipped') %}
<div class="notice warning">Didn't run — something else was using the pipeline at that moment (check <a href="/settings/jobs">Jobs</a> for what). Nothing changed; try again in a moment.</div>
{% if request.query_params.get('started') %}
<div class="notice">Started in the background. Reload this page in a bit to see the result below, or watch its progress under <a href="/settings/jobs">Jobs</a>. If the pipeline is busy it will show as skipped there.</div>
{% endif %}
<div class="panel" style="max-width:560px;">
@@ -18,7 +18,8 @@
<form method="post" action="/genres/scan" class="inline">
<button type="submit" class="btn btn-ghost">Preview changes</button>
</form>
<form method="post" action="/genres/apply" class="inline">
<form method="post" action="/genres/apply" class="inline"
onsubmit="return confirm('Overwrite genres across your whole library with what Spotify reports? Locked artists are left alone. Preview first if unsure.')">
<button type="submit" class="btn btn-primary">Fix genres now</button>
</form>
</div>
+2 -2
View File
@@ -8,8 +8,8 @@
</div>
</div>
{% if request.query_params.get('run_id') %}
<div class="notice success">Import started (run #{{ request.query_params.get('run_id') }}). Check <a href="/settings/jobs">Jobs</a> for progress.</div>
{% if request.query_params.get('started') %}
<div class="notice success">Import started in the background. Check <a href="/settings/jobs">Jobs</a> for progress and the result. If the pipeline is busy it will show as skipped there.</div>
{% endif %}
<div class="panel dropzone-panel" x-data="{ over: false }">
+3
View File
@@ -1,6 +1,9 @@
{% extends "settings/_layout.html" %}
{% block settings_title %}Jobs{% endblock %}
{% block settings_content %}
{% if request.query_params.get('started') %}
<div class="notice">Started in the background. Watch the runs table below (it refreshes on its own). If the pipeline is already busy, this run shows as skipped.</div>
{% endif %}
<h2 style="margin-top:0;">Maintenance jobs</h2>
<div class="table-wrap scroll">
<table>