Add Select All, Fix Selected & Fix All to Library Maintenance findings

This commit is contained in:
Broque Thomas 2026-03-17 09:25:21 -07:00
parent 162542a0cf
commit cc62541a65
5 changed files with 199 additions and 1 deletions

View file

@ -1043,6 +1043,55 @@ class RepairWorker:
if conn:
conn.close()
def bulk_fix_findings(self, job_id: str = None, severity: str = None,
finding_ids: List[int] = None) -> dict:
"""Fix all pending fixable findings matching filters. Returns {fixed, failed, skipped}."""
conn = None
try:
conn = self.db._get_connection()
cursor = conn.cursor()
# Build query for pending fixable findings
fixable_types = ('dead_file', 'orphan_file', 'track_number_mismatch',
'missing_cover_art', 'metadata_gap', 'duplicate_tracks', 'mbid_mismatch')
placeholders = ','.join(['?'] * len(fixable_types))
where_parts = [f"finding_type IN ({placeholders})", "status = 'pending'"]
params = list(fixable_types)
if finding_ids:
id_placeholders = ','.join(['?'] * len(finding_ids))
where_parts.append(f"id IN ({id_placeholders})")
params.extend(finding_ids)
if job_id:
where_parts.append("job_id = ?")
params.append(job_id)
if severity:
where_parts.append("severity = ?")
params.append(severity)
where = f"WHERE {' AND '.join(where_parts)}"
cursor.execute(f"SELECT id FROM repair_findings {where}", params)
ids_to_fix = [row[0] for row in cursor.fetchall()]
conn.close()
conn = None
fixed = 0
failed = 0
for fid in ids_to_fix:
result = self.fix_finding(fid)
if result.get('success'):
fixed += 1
else:
failed += 1
return {'fixed': fixed, 'failed': failed, 'total': len(ids_to_fix)}
except Exception as e:
logger.error("Error bulk fixing findings: %s", e, exc_info=True)
return {'fixed': 0, 'failed': 0, 'total': 0, 'error': str(e)}
finally:
if conn:
conn.close()
def bulk_update_findings(self, finding_ids: List[int], action: str) -> int:
"""Bulk resolve or dismiss findings. Returns count updated."""
conn = None

View file

@ -39306,6 +39306,31 @@ def repair_finding_dismiss(finding_id):
logger.error(f"Error dismissing finding {finding_id}: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/api/repair/findings/bulk-fix', methods=['POST'])
def repair_findings_bulk_fix():
"""Bulk fix all pending fixable findings matching filters"""
try:
if repair_worker is None:
return jsonify({'error': 'Repair worker not initialized'}), 400
data = request.get_json(silent=True) or {}
job_id = data.get('job_id') or None
severity = data.get('severity') or None
finding_ids = data.get('ids') or None
result = repair_worker.bulk_fix_findings(
job_id=job_id, severity=severity, finding_ids=finding_ids
)
return jsonify({
'success': True,
'fixed': result.get('fixed', 0),
'failed': result.get('failed', 0),
'total': result.get('total', 0)
}), 200
except Exception as e:
logger.error(f"Error bulk fixing findings: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/api/repair/findings/bulk', methods=['POST'])
def repair_findings_bulk():
"""Bulk resolve or dismiss findings"""

View file

@ -5794,9 +5794,15 @@
<option value="dismissed">Dismissed</option>
</select>
</div>
<label class="repair-select-all" title="Select all on this page">
<input type="checkbox" id="repair-select-all-cb" onchange="toggleSelectAllFindings(this.checked)">
<span>Select All</span>
</label>
<div class="repair-findings-bulk" id="repair-findings-bulk" style="display:none;">
<span class="repair-bulk-count" id="repair-bulk-count"></span>
<button class="repair-bulk-btn fix" onclick="bulkFixFindings()">Fix Selected</button>
<button class="repair-bulk-btn" onclick="bulkRepairAction('dismiss')">Dismiss Selected</button>
<button class="repair-bulk-btn fix-all" id="repair-fix-all-btn" style="display:none;" onclick="fixAllMatchingFindings()">Fix All</button>
</div>
<button class="repair-clear-btn" onclick="clearRepairFindings()" title="Clear findings matching current filters">Clear Findings</button>
</div>

View file

@ -51607,6 +51607,7 @@ function updateRepairStatusFromData(data) {
let _repairCurrentTab = 'jobs';
let _repairFindingsPage = 0;
let _repairSelectedFindings = new Set();
let _repairFindingsTotal = 0;
const REPAIR_FINDINGS_PAGE_SIZE = 30;
let _repairJobsCache = {}; // Cache job data for help modal
@ -52154,8 +52155,11 @@ async function loadRepairFindings() {
const items = data.items || [];
_repairSelectedFindings.clear();
_repairFindingsTotal = data.total || 0;
const bulkBar = document.getElementById('repair-findings-bulk');
if (bulkBar) bulkBar.style.display = 'none';
const selectAllCb = document.getElementById('repair-select-all-cb');
if (selectAllCb) { selectAllCb.checked = false; selectAllCb.indeterminate = false; }
if (items.length === 0) {
container.innerHTML = `<div class="repair-empty-state">
@ -52537,8 +52541,88 @@ function toggleFindingSelect(id, checked) {
if (checked) _repairSelectedFindings.add(id);
else _repairSelectedFindings.delete(id);
_updateFindingsBulkBar();
}
function _updateFindingsBulkBar() {
const bulkBar = document.getElementById('repair-findings-bulk');
if (bulkBar) bulkBar.style.display = _repairSelectedFindings.size > 0 ? '' : 'none';
const count = _repairSelectedFindings.size;
if (bulkBar) bulkBar.style.display = count > 0 ? '' : 'none';
const countEl = document.getElementById('repair-bulk-count');
if (countEl) countEl.textContent = count > 0 ? `${count} selected` : '';
// Show "Fix All (N)" when all on page are selected and there are more pages
const fixAllBtn = document.getElementById('repair-fix-all-btn');
if (fixAllBtn && _repairFindingsTotal > 0) {
const allPageSelected = count > 0 && count >= document.querySelectorAll('.repair-finding-card').length;
fixAllBtn.style.display = (allPageSelected && _repairFindingsTotal > count) ? '' : 'none';
fixAllBtn.textContent = `Fix All ${_repairFindingsTotal}`;
}
// Sync "Select All" checkbox
const selectAllCb = document.getElementById('repair-select-all-cb');
if (selectAllCb) {
const totalOnPage = document.querySelectorAll('.repair-finding-card').length;
selectAllCb.checked = totalOnPage > 0 && count >= totalOnPage;
selectAllCb.indeterminate = count > 0 && count < totalOnPage;
}
}
function toggleSelectAllFindings(checked) {
const checkboxes = document.querySelectorAll('.repair-finding-select input[type="checkbox"]');
checkboxes.forEach(cb => {
cb.checked = checked;
const card = cb.closest('.repair-finding-card');
if (card) {
const id = parseInt(card.dataset.id);
if (checked) _repairSelectedFindings.add(id);
else _repairSelectedFindings.delete(id);
}
});
_updateFindingsBulkBar();
}
async function fixAllMatchingFindings() {
const jobFilter = document.getElementById('repair-findings-job-filter');
const severityFilter = document.getElementById('repair-findings-severity-filter');
const jobId = jobFilter ? jobFilter.value : '';
const severity = severityFilter ? severityFilter.value : '';
const scopeLabel = jobId ? jobId.replace(/_/g, ' ') : 'all jobs';
if (!await showConfirmDialog({
title: 'Fix All Findings',
message: `Apply fixes to all ${_repairFindingsTotal} pending fixable findings for ${scopeLabel}? This may delete files or remove database entries depending on finding type.`,
confirmText: 'Fix All',
destructive: true
})) return;
showToast(`Fixing ${_repairFindingsTotal} findings...`, 'info');
try {
const body = {};
if (jobId) body.job_id = jobId;
if (severity) body.severity = severity;
const response = await fetch('/api/repair/findings/bulk-fix', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const result = await response.json();
if (result.success) {
showToast(`Fixed ${result.fixed}${result.failed ? `, ${result.failed} failed` : ''} of ${result.total}`, result.fixed > 0 ? 'success' : 'error');
} else {
showToast(result.error || 'Bulk fix failed', 'error');
}
} catch (error) {
console.error('Error in bulk fix:', error);
showToast('Error applying bulk fix', 'error');
}
_repairSelectedFindings.clear();
loadRepairFindingsDashboard();
loadRepairFindings();
updateRepairStatus();
}
function renderRepairFindingsPagination(total, currentPage) {

View file

@ -41158,11 +41158,35 @@ tr.tag-diff-same {
font-weight: 600;
}
.repair-select-all {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
color: rgba(255, 255, 255, 0.6);
font-size: 12px;
user-select: none;
}
.repair-select-all input[type="checkbox"] {
accent-color: rgb(var(--accent-rgb, 99, 102, 241));
cursor: pointer;
}
.repair-select-all:hover {
color: rgba(255, 255, 255, 0.9);
}
.repair-findings-bulk {
display: flex;
align-items: center;
gap: 6px;
}
.repair-bulk-count {
color: rgba(255, 255, 255, 0.6);
font-size: 12px;
margin-right: 4px;
}
.repair-bulk-btn {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
@ -41190,6 +41214,16 @@ tr.tag-diff-same {
background: rgba(var(--accent-rgb, 99, 102, 241), 0.2);
}
.repair-bulk-btn.fix-all {
background: rgba(245, 158, 11, 0.12);
border-color: rgba(245, 158, 11, 0.3);
color: #fbbf24;
font-weight: 600;
}
.repair-bulk-btn.fix-all:hover {
background: rgba(245, 158, 11, 0.22);
}
.repair-clear-btn {
margin-left: auto;
background: rgba(239, 68, 68, 0.08);