Show detailed batch repair preview

This commit is contained in:
Philip Guzman
2026-07-01 16:26:04 -07:00
parent f3cfed316d
commit ec646479d2
6 changed files with 201 additions and 2 deletions
+7
View File
@@ -360,6 +360,13 @@ def duplicate_repair_batch(
result = {
"choice_count": len(plans),
"replaced": replaced,
"decisions": [
{
"keeper": str(plan.keeper),
"replaced": [str(path) for path in plan.replaced],
}
for plan in plans
],
"metadata_backups": len(plans[0].metadata_files),
"strategy": "shortcut",
"database_v2_modified": False,
+19 -1
View File
@@ -22,6 +22,13 @@ const audioPreview = document.querySelector('#audio-preview');
const audioPreviewName = document.querySelector('#audio-preview-name');
const audioPlayer = document.querySelector('#audio-player');
const closeAudioPreview = document.querySelector('#close-audio-preview');
const batchPreviewModal = document.querySelector('#batch-preview-modal');
const batchPreviewSummary = document.querySelector('#batch-preview-summary');
const batchPreviewList = document.querySelector('#batch-preview-list');
const batchPreviewSafety = document.querySelector('#batch-preview-safety');
const closeBatchPreview = document.querySelector('#close-batch-preview');
const continueReviewing = document.querySelector('#continue-reviewing');
const acceptPreview = document.querySelector('#accept-preview');
let latestAnalysis = null;
let selectedDuplicateGroup = null;
let previewedRepair = null;
@@ -191,12 +198,23 @@ previewRepairButton.addEventListener('click', async () => {
repairMessage.textContent = 'Checking the plan…'; applyRepairButton.disabled = true;
try {
previewedRepair = await requestRepair('/api/duplicates/batch/preview');
repairPreview.innerHTML = `<strong>One safe plan for ${previewedRepair.choice_count} approved group(s)</strong><p>${previewedRepair.replaced.length} duplicate file(s) will be backed up, then replaced with shortcuts to their selected keepers. ${previewedRepair.metadata_backups} Serato metadata file(s) will also be copied into the rollback snapshot. Database V2 will not be changed.</p>`;
batchPreviewSummary.textContent = `${previewedRepair.choice_count} keeper decision(s) · ${previewedRepair.replaced.length} duplicate file(s) consolidated`;
batchPreviewList.innerHTML = previewedRepair.decisions.map((decision, index) => `
<article class="preview-decision"><span>Decision ${index + 1}</span><div class="winner-path"><b>Keep</b><strong>${escapeHtml(decision.keeper.split('/').pop())}</strong><small>${escapeHtml(decision.keeper)}</small></div><div class="replaced-paths"><b>Replace with a shortcut</b>${decision.replaced.map((path) => `<small>${escapeHtml(path)}</small>`).join('')}</div></article>
`).join('');
batchPreviewSafety.textContent = `${previewedRepair.metadata_backups} Serato metadata file(s) and every replaced audio file will be backed up. Database V2 will not be modified.`;
repairPreview.innerHTML = `<strong>Preview approved</strong><p>${previewedRepair.choice_count} keeper decision(s) are ready for one backed-up apply.</p>`;
repairPreview.hidden = false; applyRepairButton.disabled = false;
repairMessage.textContent = 'Preview complete. Nothing has changed yet.';
batchPreviewModal.showModal();
} catch (error) { repairMessage.textContent = error.message; }
});
function dismissBatchPreview() { batchPreviewModal.close(); }
closeBatchPreview.addEventListener('click', dismissBatchPreview);
continueReviewing.addEventListener('click', dismissBatchPreview);
acceptPreview.addEventListener('click', dismissBatchPreview);
applyRepairButton.addEventListener('click', async () => {
if (!previewedRepair) return;
applyRepairButton.disabled = true; repairMessage.textContent = 'Creating the backup before making changes…';
+8 -1
View File
@@ -121,8 +121,15 @@
<audio id="audio-player" controls preload="metadata"></audio>
<button id="close-audio-preview" type="button" aria-label="Close audio preview">×</button>
</div>
<dialog id="batch-preview-modal" class="batch-preview-modal">
<div class="modal-heading"><div><p class="eyebrow">Final dry-run</p><h2>Heres exactly what will happen</h2></div><button id="close-batch-preview" type="button" aria-label="Close preview">×</button></div>
<p id="batch-preview-summary"></p>
<div id="batch-preview-list" class="batch-preview-list"></div>
<div class="modal-safety"><span></span><p><strong>Backup happens first</strong><small id="batch-preview-safety"></small></p></div>
<div class="modal-actions"><button id="continue-reviewing" type="button">Close and continue reviewing</button><button id="accept-preview" type="button">Looks right</button></div>
</dialog>
</main>
</div>
<script src="/app.js?v=5" defer></script>
<script src="/app.js?v=6" defer></script>
</body>
</html>
+158
View File
@@ -139,6 +139,164 @@
}
}
.batch-preview-modal {
width: min(820px, calc(100vw - 32px));
max-height: min(82vh, 760px);
padding: 24px;
overflow: auto;
border: 1px solid rgba(155, 135, 245, .3);
border-radius: 20px;
background: #161923;
color: var(--text);
box-shadow: 0 30px 100px rgba(0, 0, 0, .65);
}
.batch-preview-modal::backdrop {
background: rgba(4, 5, 8, .78);
backdrop-filter: blur(7px);
}
.modal-heading,
.modal-actions,
.modal-safety {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
}
.modal-heading h2 {
margin: 0;
font-size: 23px;
letter-spacing: -.03em;
}
.modal-heading > button {
border: 0;
background: transparent;
color: var(--muted);
font-size: 28px;
cursor: pointer;
}
#batch-preview-summary {
color: var(--muted);
font-size: 12px;
}
.batch-preview-list {
display: grid;
gap: 10px;
max-height: 390px;
margin: 18px 0;
padding-right: 4px;
overflow: auto;
}
.preview-decision {
display: grid;
grid-template-columns: 80px 1fr 1fr;
gap: 13px;
padding: 14px;
border: 1px solid var(--line);
border-radius: 13px;
background: rgba(255, 255, 255, .025);
}
.preview-decision > span,
.preview-decision b {
color: var(--muted);
font-size: 9px;
text-transform: uppercase;
letter-spacing: .08em;
}
.winner-path,
.replaced-paths {
min-width: 0;
}
.winner-path strong,
.winner-path small,
.replaced-paths small {
display: block;
margin-top: 5px;
overflow-wrap: anywhere;
}
.winner-path strong {
color: #8ce4b8;
font-size: 11px;
}
.winner-path small,
.replaced-paths small {
color: var(--muted);
font-size: 9px;
line-height: 1.45;
}
.modal-safety {
justify-content: flex-start;
padding: 13px;
border: 1px solid rgba(102, 217, 232, .2);
border-radius: 12px;
background: rgba(102, 217, 232, .05);
}
.modal-safety > span {
color: var(--cyan);
}
.modal-safety p,
.modal-safety strong,
.modal-safety small {
display: block;
margin: 0;
}
.modal-safety strong {
font-size: 11px;
}
.modal-safety small {
margin-top: 4px;
color: var(--muted);
font-size: 9px;
line-height: 1.45;
}
.modal-actions {
justify-content: flex-end;
margin-top: 16px;
}
.modal-actions button {
border: 1px solid var(--line);
border-radius: 10px;
padding: 10px 13px;
background: rgba(255, 255, 255, .04);
color: var(--text);
font: 700 11px/1 inherit;
cursor: pointer;
}
#accept-preview {
border-color: transparent;
background: #6f5bd0;
}
@media (max-width: 620px) {
.preview-decision {
grid-template-columns: 1fr;
}
.modal-actions {
align-items: stretch;
flex-direction: column-reverse;
}
}
.file-compare-row {
display: grid;
gap: 8px;