Merge feature/quarantine-ui-consolidation: consolidate quarantine UI
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
bdbeff89d6
4 changed files with 71 additions and 280 deletions
|
|
@ -8158,9 +8158,6 @@
|
|||
<button class="library-history-tab" data-tab="import" onclick="switchHistoryTab('import')">
|
||||
Server Imports <span class="library-history-tab-count" id="history-import-count">0</span>
|
||||
</button>
|
||||
<button class="library-history-tab" data-tab="quarantine" onclick="switchHistoryTab('quarantine')">
|
||||
Quarantine <span class="library-history-tab-count" id="history-quarantine-count">0</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="history-source-bar" id="history-source-bar" style="display:none"></div>
|
||||
<div class="library-history-list" id="library-history-list"></div>
|
||||
|
|
|
|||
|
|
@ -2668,47 +2668,72 @@ const _VERIF_QUAR_TRIGGERS = {
|
|||
bit_depth: ['BIT DEPTH FILTER', 'verif-rb-int'],
|
||||
};
|
||||
|
||||
function _verifQuarRowHtml(q, idx) {
|
||||
const title = _adlEsc(q.expected_track || q.original_filename || q.filename || 'Unknown file');
|
||||
const meta = [_adlEsc(q.expected_artist || ''), _adlEsc(q.original_filename || '')].filter(Boolean).join(' — ');
|
||||
const [trigLabel, trigClass] = _VERIF_QUAR_TRIGGERS[q.trigger] || ['QUARANTINED', 'verif-rb-unv'];
|
||||
const timeAgo = _verifTimeAgo(q.timestamp);
|
||||
const approveBtn = q.has_full_context
|
||||
? `<button class="verif-act verif-act-ok" onclick="verifQuarApprove(${idx}, this)" title="Approve: re-import this exact file into the library, marked human-verified">✔</button>`
|
||||
: `<button class="verif-act verif-act-ok" onclick="verifQuarRecover(${idx}, this)" title="Recover to Staging for a manual import (legacy entry without embedded context)">⤴</button>`;
|
||||
const details = [
|
||||
q.reason ? `<div><span class="verif-detail-label">Reason:</span> ${_adlEsc(q.reason)}</div>` : '',
|
||||
q.source_username ? `<div><span class="verif-detail-label">Source uploader:</span> ${_adlEsc(q.source_username)}</div>` : '',
|
||||
q.source_filename ? `<div><span class="verif-detail-label">Original Soulseek file:</span> ${_adlEsc(q.source_filename)}</div>` : '',
|
||||
q.timestamp ? `<div><span class="verif-detail-label">Quarantined:</span> ${_adlEsc(q.timestamp)}</div>` : '',
|
||||
].filter(Boolean).join('');
|
||||
const detailsOpen = _verifQuarOpenDetails.has(q.id);
|
||||
const artHtml = q.thumb_url
|
||||
? `<img class="adl-row-art" src="${_adlEsc(q.thumb_url)}" alt="" onerror="this.style.display='none'">`
|
||||
: '<div class="adl-row-art adl-row-art-empty"></div>';
|
||||
return `<div class="adl-row adl-row-failed verif-quar-row" data-quarantine-id="${_adlEsc(q.id)}" onclick="verifQuarInspect(${idx})" title="Click to show/hide details (reason, source uploader, original filename)">
|
||||
${artHtml}
|
||||
<div class="adl-row-info">
|
||||
<div class="adl-row-title">${title}</div>
|
||||
${meta ? `<div class="adl-row-meta">${meta}</div>` : ''}
|
||||
<div class="verif-quar-details" id="verif-quar-details-${idx}" style="display:${detailsOpen ? '' : 'none'}">${details || 'No further details in the sidecar.'}</div>
|
||||
</div>
|
||||
<div class="verif-actions" onclick="event.stopPropagation()">
|
||||
<span class="verif-reason-badge ${trigClass}" title="${_adlEsc(q.reason || '')}">${trigLabel}</span>
|
||||
${q.quality ? `<span class="adl-quality-chip" title="Audio quality of the quarantined file (read from the file itself)">${_adlEsc(q.quality)}</span>` : ''}
|
||||
${timeAgo ? `<span class="verif-time">${timeAgo}</span>` : ''}
|
||||
<button class="verif-act verif-act-play" onclick="verifQuarPlay(${idx})" title="Play the quarantined file in the media player">▶</button>
|
||||
<button class="verif-act" onclick="verifQuarCompare(${idx}, this)" title="Find the expected track on Soulseek/streaming sources and play it in the media player — compare against the quarantined file">⇆</button>
|
||||
<button class="verif-act" onclick="verifQuarAudit(${idx})" title="Open the audit trail for this quarantined file (details, embedded tags, lyrics)">🔍</button>
|
||||
${approveBtn}
|
||||
<button class="verif-act verif-act-del" onclick="verifQuarDelete(${idx}, this)" title="Delete the quarantined file permanently">🗑</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function _verifQuarRows() {
|
||||
if (!_verifQuarLoaded) return '<div class="adl-section-header">Loading quarantine…</div>';
|
||||
if (!_verifQuarEntries.length) return '';
|
||||
const idxById = new Map(_verifQuarEntries.map((q, i) => [q.id, i]));
|
||||
const groups = (typeof _groupQuarantineEntries === 'function')
|
||||
? _groupQuarantineEntries(_verifQuarEntries)
|
||||
: _verifQuarEntries.map(q => ({ key: null, members: [q] }));
|
||||
let html = '';
|
||||
_verifQuarEntries.forEach((q, idx) => {
|
||||
const title = _adlEsc(q.expected_track || q.original_filename || q.filename || 'Unknown file');
|
||||
const meta = [_adlEsc(q.expected_artist || ''), _adlEsc(q.original_filename || '')].filter(Boolean).join(' — ');
|
||||
const [trigLabel, trigClass] = _VERIF_QUAR_TRIGGERS[q.trigger] || ['QUARANTINED', 'verif-rb-unv'];
|
||||
const timeAgo = _verifTimeAgo(q.timestamp);
|
||||
const approveBtn = q.has_full_context
|
||||
? `<button class="verif-act verif-act-ok" onclick="verifQuarApprove(${idx}, this)" title="Approve: re-import this exact file into the library, marked human-verified">✔</button>`
|
||||
: `<button class="verif-act verif-act-ok" onclick="verifQuarRecover(${idx}, this)" title="Recover to Staging for a manual import (legacy entry without embedded context)">⤴</button>`;
|
||||
const details = [
|
||||
q.reason ? `<div><span class="verif-detail-label">Reason:</span> ${_adlEsc(q.reason)}</div>` : '',
|
||||
q.source_username ? `<div><span class="verif-detail-label">Source uploader:</span> ${_adlEsc(q.source_username)}</div>` : '',
|
||||
q.source_filename ? `<div><span class="verif-detail-label">Original Soulseek file:</span> ${_adlEsc(q.source_filename)}</div>` : '',
|
||||
q.timestamp ? `<div><span class="verif-detail-label">Quarantined:</span> ${_adlEsc(q.timestamp)}</div>` : '',
|
||||
].filter(Boolean).join('');
|
||||
const detailsOpen = _verifQuarOpenDetails.has(q.id);
|
||||
const artHtml = q.thumb_url
|
||||
? `<img class="adl-row-art" src="${_adlEsc(q.thumb_url)}" alt="" onerror="this.style.display='none'">`
|
||||
: '<div class="adl-row-art adl-row-art-empty"></div>';
|
||||
html += `<div class="adl-row adl-row-failed verif-quar-row" data-quarantine-id="${_adlEsc(q.id)}" onclick="verifQuarInspect(${idx})" title="Click to show/hide details (reason, source uploader, original filename)">
|
||||
${artHtml}
|
||||
<div class="adl-row-info">
|
||||
<div class="adl-row-title">${title}</div>
|
||||
${meta ? `<div class="adl-row-meta">${meta}</div>` : ''}
|
||||
<div class="verif-quar-details" id="verif-quar-details-${idx}" style="display:${detailsOpen ? '' : 'none'}">${details || 'No further details in the sidecar.'}</div>
|
||||
</div>
|
||||
<div class="verif-actions" onclick="event.stopPropagation()">
|
||||
<span class="verif-reason-badge ${trigClass}" title="${_adlEsc(q.reason || '')}">${trigLabel}</span>
|
||||
${q.quality ? `<span class="adl-quality-chip" title="Audio quality of the quarantined file (read from the file itself)">${_adlEsc(q.quality)}</span>` : ''}
|
||||
${timeAgo ? `<span class="verif-time">${timeAgo}</span>` : ''}
|
||||
<button class="verif-act verif-act-play" onclick="verifQuarPlay(${idx})" title="Play the quarantined file in the media player">▶</button>
|
||||
<button class="verif-act" onclick="verifQuarCompare(${idx}, this)" title="Find the expected track on Soulseek/streaming sources and play it in the media player — compare against the quarantined file">⇆</button>
|
||||
<button class="verif-act" onclick="verifQuarAudit(${idx})" title="Open the audit trail for this quarantined file (details, embedded tags, lyrics)">🔍</button>
|
||||
${approveBtn}
|
||||
<button class="verif-act verif-act-del" onclick="verifQuarDelete(${idx}, this)" title="Delete the quarantined file permanently">🗑</button>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
for (const group of groups) {
|
||||
if (group.members.length === 1) {
|
||||
html += _verifQuarRowHtml(group.members[0], idxById.get(group.members[0].id));
|
||||
} else {
|
||||
const first = group.members[0];
|
||||
const title = _adlEsc(first.expected_track || first.original_filename || 'Unknown');
|
||||
const artist = first.expected_artist ? ` — ${_adlEsc(first.expected_artist)}` : '';
|
||||
const n = group.members.length;
|
||||
const groupId = `vqg-${_adlEsc(first.id)}`;
|
||||
html += `<div class="verif-quar-group">
|
||||
<div class="verif-quar-group-hdr" onclick="document.getElementById('${groupId}').classList.toggle('vqg-open')" title="${n} alternative candidates quarantined for this track">
|
||||
<span class="verif-quar-group-lbl">${title}${artist}</span>
|
||||
<span class="verif-quar-group-cnt">${n} alternatives ▾</span>
|
||||
</div>
|
||||
<div class="verif-quar-group-members" id="${groupId}">
|
||||
${group.members.map(m => _verifQuarRowHtml(m, idxById.get(m.id))).join('')}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -68215,6 +68215,14 @@ body.em-scroll-lock { overflow: hidden; }
|
|||
.verif-banner-spacer { flex: 1; }
|
||||
.verif-bulk-danger { border-color: rgba(248,113,113,0.4) !important; color: #f87171 !important; }
|
||||
|
||||
.verif-quar-group { margin-bottom: 4px; }
|
||||
.verif-quar-group-hdr { display: flex; align-items: center; gap: 8px; padding: 6px 12px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 6px; cursor: pointer; user-select: none; }
|
||||
.verif-quar-group-hdr:hover { background: rgba(255,255,255,0.07); }
|
||||
.verif-quar-group-lbl { flex: 1; font-size: 13px; font-weight: 500; color: rgba(255,255,255,0.85); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.verif-quar-group-cnt { font-size: 12px; color: rgba(255,255,255,0.45); white-space: nowrap; }
|
||||
.verif-quar-group-members { display: none; padding-left: 12px; padding-top: 2px; }
|
||||
.verif-quar-group-members.vqg-open { display: block; }
|
||||
|
||||
/* ── Security settings: grouped sub-sections + dependency visuals ── */
|
||||
.security-subgroup {
|
||||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||
|
|
|
|||
|
|
@ -3253,61 +3253,10 @@ function openLibraryHistoryModal() {
|
|||
overlay.classList.remove('hidden');
|
||||
_libraryHistoryState.page = 1;
|
||||
loadLibraryHistory();
|
||||
_refreshQuarantineTabCount(); // #876: count correct on open, not only after clicking the tab
|
||||
}
|
||||
}
|
||||
|
||||
// #876: keep the Quarantine tab badge accurate the moment the modal opens. The
|
||||
// full count was previously only set by loadQuarantineList() (i.e. after the tab
|
||||
// was clicked), so it showed a stale 0 until then.
|
||||
async function _refreshQuarantineTabCount() {
|
||||
const el = document.getElementById('history-quarantine-count');
|
||||
if (!el) return;
|
||||
try {
|
||||
const resp = await fetch('/api/quarantine/list');
|
||||
const data = await resp.json();
|
||||
el.textContent = (data.entries || []).length;
|
||||
} catch (e) { /* leave the existing value on error */ }
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Quarantine tab — rendered inside the Library History modal as a third
|
||||
// tab next to Downloads + Server Imports. Reuses the existing list +
|
||||
// pagination chrome; provides per-row Approve / Recover / Delete actions.
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function loadQuarantineList() {
|
||||
const list = document.getElementById('library-history-list');
|
||||
const pagination = document.getElementById('library-history-pagination');
|
||||
const sourceBar = document.getElementById('history-source-bar');
|
||||
if (!list) return;
|
||||
list.innerHTML = '<div class="library-history-loading">Loading...</div>';
|
||||
if (pagination) pagination.innerHTML = '';
|
||||
if (sourceBar) sourceBar.style.display = 'none';
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/quarantine/list');
|
||||
const data = await resp.json();
|
||||
const entries = data.entries || [];
|
||||
const countEl = document.getElementById('history-quarantine-count');
|
||||
if (countEl) countEl.textContent = entries.length;
|
||||
|
||||
if (!data.success) {
|
||||
list.innerHTML = `<div class="library-history-empty">Error: ${escapeHtml(data.error || 'Failed to load')}</div>`;
|
||||
return;
|
||||
}
|
||||
if (entries.length === 0) {
|
||||
list.innerHTML = '<div class="library-history-empty">🛡️<br><br>No quarantined files. Nice and clean.</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = _groupQuarantineEntries(entries).map(renderQuarantineGroupOrEntry).join('');
|
||||
} catch (err) {
|
||||
console.error('Error loading quarantine entries:', err);
|
||||
list.innerHTML = '<div class="library-history-empty">Error loading quarantine</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// #876: bucket entries that are alternatives for the SAME intended target
|
||||
// Bucket entries that are alternatives for the SAME intended target
|
||||
// (same backend group_key — derived from expected_artist/expected_track, the
|
||||
// track SoulSync was trying to fetch, not the bad file's own tags). Entries
|
||||
// with a null group_key (legacy/orphan, ungroupable) each stand alone.
|
||||
|
|
@ -3325,188 +3274,6 @@ function _groupQuarantineEntries(entries) {
|
|||
return groups;
|
||||
}
|
||||
|
||||
function renderQuarantineGroupOrEntry(group) {
|
||||
if (group.members.length === 1) return renderQuarantineEntry(group.members[0]);
|
||||
return renderQuarantineGroup(group);
|
||||
}
|
||||
|
||||
// A collapsible parent row for multiple alternatives of one song. Header shows
|
||||
// the shared track/artist + an alternatives count; members reuse the standard
|
||||
// entry markup so per-row Approve/Recover/Delete keep working unchanged.
|
||||
function renderQuarantineGroup(group) {
|
||||
const first = group.members[0] || {};
|
||||
const title = first.expected_track || first.original_filename || 'Unknown';
|
||||
const artist = first.expected_artist || '';
|
||||
const n = group.members.length;
|
||||
const sub = artist ? escapeHtml(artist) : '';
|
||||
// Reuse the album art the sidecar context carried, when any member has it.
|
||||
const thumb = (group.members.find(m => m.thumb_url) || {}).thumb_url || '';
|
||||
const thumbHtml = thumb
|
||||
? `<img class="library-history-thumb" src="${escapeHtml(thumb)}" alt="" onerror="this.style.display='none'">`
|
||||
: '<div class="library-history-thumb-placeholder">🛡️</div>';
|
||||
return `<div class="lh-quarantine-group lh-qgroup-collapsed">
|
||||
<div class="lh-quarantine-group-header" onclick="this.parentElement.classList.toggle('lh-qgroup-collapsed')">
|
||||
${thumbHtml}
|
||||
<div class="lh-quarantine-group-text">
|
||||
<div class="library-history-entry-title">${escapeHtml(title)}</div>
|
||||
<div class="library-history-entry-meta">${sub}</div>
|
||||
</div>
|
||||
<span class="lh-quarantine-group-count">${n} alternatives</span>
|
||||
<span class="lh-expand-btn">▾</span>
|
||||
</div>
|
||||
<div class="lh-quarantine-group-members">
|
||||
${group.members.map(renderQuarantineEntry).join('')}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderQuarantineEntry(entry) {
|
||||
const triggerLabels = { integrity: 'Duration / Integrity', acoustid: 'AcoustID Mismatch', bit_depth: 'Bit Depth Filter', unknown: 'Unknown' };
|
||||
const triggerColors = { integrity: '#facc15', acoustid: '#ef5350', bit_depth: '#fb923c', unknown: '#888' };
|
||||
const triggerLabel = triggerLabels[entry.trigger] || entry.trigger || 'Unknown';
|
||||
const triggerColor = triggerColors[entry.trigger] || '#888';
|
||||
|
||||
// Keep dynamic filenames/ids out of inline JS. Quarantine ids are derived
|
||||
// from filenames, so quotes in the filename can break onclick attributes
|
||||
// if they are interpolated as JS string literals.
|
||||
const entryIdAttr = escapeHtml(String(entry.id || ''));
|
||||
const approveLabel = entry.has_full_context ? 'Approve' : 'Recover';
|
||||
const approveTitle = entry.has_full_context
|
||||
? 'Re-run post-processing with quarantine checks skipped for this approved file'
|
||||
: 'Legacy entry — move to Staging, finish via Import flow';
|
||||
const approveCall = entry.has_full_context
|
||||
? 'approveQuarantineEntryFromButton(this)'
|
||||
: 'recoverQuarantineEntryFromButton(this)';
|
||||
|
||||
const meta = [entry.expected_artist, entry.original_filename].filter(Boolean).join(' — ');
|
||||
const triggerBadge = `<span class="library-history-badge" style="border-color:${triggerColor};color:${triggerColor}">${escapeHtml(triggerLabel)}</span>`;
|
||||
const reasonDetail = `<div class="library-history-entry-source"><span class="lh-prov-label">Reason:</span> ${escapeHtml(entry.reason || 'Unknown')}</div>`;
|
||||
|
||||
// Issue #608 follow-up: show source uploader + original soulseek
|
||||
// filename when the sidecar carried that context. Helps the user
|
||||
// understand which uploader the bad file came from at a glance.
|
||||
const sourceParts = [];
|
||||
if (entry.source_username) {
|
||||
sourceParts.push(`<div class="library-history-entry-source"><span class="lh-prov-label">Source:</span> ${escapeHtml(entry.source_username)}</div>`);
|
||||
}
|
||||
if (entry.source_filename) {
|
||||
sourceParts.push(`<div class="library-history-entry-source"><span class="lh-prov-label">Original filename:</span> ${escapeHtml(entry.source_filename)}</div>`);
|
||||
}
|
||||
const sourceDetail = sourceParts.join('');
|
||||
|
||||
return `<div class="library-history-entry lh-expandable" onclick="this.classList.toggle('lh-expanded')">
|
||||
<div class="library-history-thumb-placeholder">🛡️</div>
|
||||
<div class="library-history-entry-content">
|
||||
<div class="library-history-entry-row1">
|
||||
<div class="library-history-entry-text">
|
||||
<div class="library-history-entry-title">${escapeHtml(entry.expected_track || entry.original_filename || 'Unknown')}</div>
|
||||
<div class="library-history-entry-meta">${escapeHtml(meta)}</div>
|
||||
</div>
|
||||
<div class="library-history-entry-badges">${triggerBadge}</div>
|
||||
<div class="library-history-entry-time">${formatHistoryTime(entry.timestamp)}</div>
|
||||
<button class="lh-audit-btn" title="${approveTitle}" data-entry-id="${entryIdAttr}" onclick="event.stopPropagation();${approveCall}">${approveLabel}</button>
|
||||
<button class="lh-audit-btn" title="Delete permanently" data-entry-id="${entryIdAttr}" style="border-color:rgba(248,113,113,0.4);color:#f87171" onclick="event.stopPropagation();deleteQuarantineEntryFromButton(this)">Delete</button>
|
||||
<span class="lh-expand-btn">▾</span>
|
||||
</div>
|
||||
<div class="library-history-entry-details">
|
||||
${reasonDetail}
|
||||
${sourceDetail}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function getQuarantineEntryIdFromButton(button) {
|
||||
return button?.dataset?.entryId || '';
|
||||
}
|
||||
|
||||
function approveQuarantineEntryFromButton(button) {
|
||||
return approveQuarantineEntry(getQuarantineEntryIdFromButton(button));
|
||||
}
|
||||
|
||||
function recoverQuarantineEntryFromButton(button) {
|
||||
return recoverQuarantineEntry(getQuarantineEntryIdFromButton(button));
|
||||
}
|
||||
|
||||
function deleteQuarantineEntryFromButton(button) {
|
||||
return deleteQuarantineEntry(getQuarantineEntryIdFromButton(button));
|
||||
}
|
||||
|
||||
async function approveQuarantineEntry(entryId) {
|
||||
const ok = await showConfirmDialog({
|
||||
title: 'Approve Quarantined File',
|
||||
message: 'Re-run post-processing for this file with quarantine checks skipped for this approved pass. The file will be tagged, lyrics generated, and moved into your library.',
|
||||
confirmText: 'Approve & Import',
|
||||
cancelText: 'Cancel',
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
const r = await fetch(`/api/quarantine/${encodeURIComponent(entryId)}/approve`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
// #876: clear the other quarantined alternatives for this same song
|
||||
// once one is accepted — they're redundant failed attempts now.
|
||||
body: JSON.stringify({ remove_siblings: true }),
|
||||
});
|
||||
const data = await r.json();
|
||||
if (!data.success) {
|
||||
showToast(`Approve failed: ${data.error}`, 'error');
|
||||
} else {
|
||||
const removed = (data.removed_siblings || []).length;
|
||||
const extra = removed ? ` — removed ${removed} other option${removed === 1 ? '' : 's'}.` : '';
|
||||
showToast(`Approved — skipped ${data.trigger_bypassed} check, re-running pipeline.${extra}`, 'success');
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(`Approve failed: ${err.message}`, 'error');
|
||||
}
|
||||
loadQuarantineList();
|
||||
}
|
||||
|
||||
async function recoverQuarantineEntry(entryId) {
|
||||
const ok = await showConfirmDialog({
|
||||
title: 'Recover To Staging',
|
||||
message: 'Legacy entry — no embedded context. The file will be moved to your Staging folder so you can finish via the Import page (manual match).',
|
||||
confirmText: 'Move To Staging',
|
||||
cancelText: 'Cancel',
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
const r = await fetch(`/api/quarantine/${encodeURIComponent(entryId)}/recover`, { method: 'POST' });
|
||||
const data = await r.json();
|
||||
if (!data.success) {
|
||||
showToast(`Recover failed: ${data.error}`, 'error');
|
||||
} else {
|
||||
showToast('Moved to Staging — finish via the Import page.', 'success');
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(`Recover failed: ${err.message}`, 'error');
|
||||
}
|
||||
loadQuarantineList();
|
||||
}
|
||||
|
||||
async function deleteQuarantineEntry(entryId) {
|
||||
const ok = await showConfirmDialog({
|
||||
title: 'Delete Quarantined File',
|
||||
message: 'This permanently removes the file and its metadata sidecar. Cannot be undone.',
|
||||
confirmText: 'Delete',
|
||||
cancelText: 'Cancel',
|
||||
destructive: true,
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
const r = await fetch(`/api/quarantine/${encodeURIComponent(entryId)}`, { method: 'DELETE' });
|
||||
const data = await r.json();
|
||||
if (!data.success) {
|
||||
showToast(`Delete failed: ${data.error}`, 'error');
|
||||
} else {
|
||||
showToast('Quarantined file deleted.', 'success');
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(`Delete failed: ${err.message}`, 'error');
|
||||
}
|
||||
loadQuarantineList();
|
||||
}
|
||||
|
||||
function closeLibraryHistoryModal() {
|
||||
const overlay = document.getElementById('library-history-overlay');
|
||||
if (overlay) overlay.classList.add('hidden');
|
||||
|
|
@ -3523,12 +3290,6 @@ function switchHistoryTab(tab) {
|
|||
|
||||
async function loadLibraryHistory() {
|
||||
const { tab, page, limit } = _libraryHistoryState;
|
||||
if (tab === 'quarantine') {
|
||||
// Refresh the count for the other two tabs in the background so
|
||||
// the badge stays accurate when the user switches over.
|
||||
loadQuarantineList();
|
||||
return;
|
||||
}
|
||||
const list = document.getElementById('library-history-list');
|
||||
const pagination = document.getElementById('library-history-pagination');
|
||||
if (!list) return;
|
||||
|
|
|
|||
Loading…
Reference in a new issue