Show detailed batch repair preview
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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…';
|
||||
|
||||
@@ -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>Here’s 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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user