Quarantine: flip the modal row to Completed after Accept & Import

Accepting a quarantined item re-imported the file correctly, but the download
modal kept showing 'Quarantined'. The re-import ran through the inner pipeline,
which doesn't mark task completion (that's the verification wrapper's job), and
the sidecar context had no task_id anyway (popped before quarantine).

The chooser's Accept now sends the originating task_id, and the endpoint
re-runs the import through the verification wrapper with that task_id (+ batch_id
looked up from the task), so the task is marked completed only after the file is
verified moved — the row flips to Completed on the next poll. Manager-tab
approvals (no task_id, no JSON body — handled via get_json(silent=True)) keep
the original inner-pipeline path.

Also clear has-candidates + the quarantine dataset on every status render so a
row that goes quarantined -> completed doesn't keep a stale chooser attached.
This commit is contained in:
BoulderBadgeDad 2026-05-31 18:28:44 -07:00
parent 3060678f29
commit bad3eb1fab
2 changed files with 41 additions and 10 deletions

View file

@ -7117,14 +7117,33 @@ def approve_quarantine_item(entry_id):
# for this one restored pass so multi-reason failures do not loop.
context['_skip_quarantine_check'] = 'all'
context['_approved_quarantine_trigger'] = trigger
# Re-dispatch through the same pipeline. Run async so the HTTP
# request returns quickly — UI polls /list to see the entry vanish.
# If the caller (download-modal chooser) passed the originating task, run
# the re-import through the verification WRAPPER with that task_id so the
# task is marked completed on success — otherwise the modal row stays
# stuck on "Quarantined" even though the file imported. The sidecar
# context lost task_id/batch_id (the wrapper pops them before quarantine),
# so we re-supply them here. Manager-tab approvals (no task_id) keep the
# original inner-pipeline path.
_req = request.get_json(silent=True) or {}
_task_id = (_req.get('task_id') or '').strip() or None
_batch_id = None
if _task_id:
with tasks_lock:
_t = download_tasks.get(_task_id)
if isinstance(_t, dict):
_batch_id = _t.get('batch_id')
context['task_id'] = _task_id
if _batch_id:
context['batch_id'] = _batch_id
context_key = f"approve_{entry_id}_{int(time.time())}"
threading.Thread(
target=lambda: _post_process_matched_download(context_key, context, restored_path),
daemon=True,
).start()
logger.info(f"[Quarantine] Approved {entry_id} (original_trigger={trigger}, bypass=all) → re-running pipeline")
if _task_id:
_reprocess = lambda: _post_process_matched_download_with_verification(
context_key, context, restored_path, _task_id, _batch_id,
)
else:
_reprocess = lambda: _post_process_matched_download(context_key, context, restored_path)
threading.Thread(target=_reprocess, daemon=True).start()
logger.info(f"[Quarantine] Approved {entry_id} (original_trigger={trigger}, bypass=all, task={_task_id}) → re-running pipeline")
return jsonify({"success": True, "trigger_bypassed": "all", "original_trigger": trigger})
except Exception as e:
logger.error(f"[Quarantine] Error approving {entry_id}: {e}")

View file

@ -3449,7 +3449,7 @@ function showQuarantineChooser({ entryId, taskId, reason, trackName }) {
requestAnimationFrame(() => overlay.classList.add('visible'));
const acceptBtn = overlay.querySelector('#qc-accept-btn');
if (acceptBtn) acceptBtn.addEventListener('click', () => acceptQuarantineFromChooser(acceptBtn, entryId));
if (acceptBtn) acceptBtn.addEventListener('click', () => acceptQuarantineFromChooser(acceptBtn, entryId, taskId));
const searchBtn = overlay.querySelector('#qc-search-btn');
if (searchBtn) searchBtn.addEventListener('click', () => {
closeQuarantineChooser();
@ -3457,7 +3457,7 @@ function showQuarantineChooser({ entryId, taskId, reason, trackName }) {
});
}
async function acceptQuarantineFromChooser(button, entryId) {
async function acceptQuarantineFromChooser(button, entryId, taskId) {
if (!entryId) { showToast('Cannot accept — missing quarantine id.', 'error'); return; }
const confirmed = await showConfirmDialog({
title: 'Accept Quarantined File',
@ -3473,7 +3473,11 @@ async function acceptQuarantineFromChooser(button, entryId) {
// Release the preview stream first so the file isn't locked during the move.
await _qcReleaseAudio();
try {
const resp = await fetch(`/api/quarantine/${encodeURIComponent(entryId)}/approve`, { method: 'POST' });
const resp = await fetch(`/api/quarantine/${encodeURIComponent(entryId)}/approve`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ task_id: taskId || '' }),
});
const data = await resp.json();
if (data.success) {
showToast('Accepted. Re-running post-processing.', 'success');
@ -3770,6 +3774,14 @@ function processModalStatusUpdate(playlistId, data) {
statusEl.classList.remove('has-error-tooltip');
statusEl.removeAttribute('title');
statusEl.removeAttribute('data-error-msg');
// Clear clickable/quarantine state each render; the failure
// branch below re-adds it when still applicable. Without this a
// task that flips failed/quarantined -> completed (e.g. after
// Accept & Import) keeps a stale chooser on the cell.
statusEl.classList.remove('has-candidates');
delete statusEl.dataset.quarantineEntryId;
delete statusEl.dataset.quarantineReason;
delete statusEl.dataset.quarantineTrack;
statusEl.textContent = statusText;
if ((task.status === 'failed' || task.status === 'cancelled' || task.status === 'not_found') && task.error_message) {