From cc62541a65a411ce7a5521ba2613695e122243eb Mon Sep 17 00:00:00 2001
From: Broque Thomas <26755000+Nezreka@users.noreply.github.com>
Date: Tue, 17 Mar 2026 09:25:21 -0700
Subject: [PATCH] Add Select All, Fix Selected & Fix All to Library Maintenance
findings
---
core/repair_worker.py | 49 ++++++++++++++++++++++++
web_server.py | 25 ++++++++++++
webui/index.html | 6 +++
webui/static/script.js | 86 +++++++++++++++++++++++++++++++++++++++++-
webui/static/style.css | 34 +++++++++++++++++
5 files changed, 199 insertions(+), 1 deletion(-)
diff --git a/core/repair_worker.py b/core/repair_worker.py
index facb4cdf..0fb4361f 100644
--- a/core/repair_worker.py
+++ b/core/repair_worker.py
@@ -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
diff --git a/web_server.py b/web_server.py
index 84ef0683..ca5b38c4 100644
--- a/web_server.py
+++ b/web_server.py
@@ -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"""
diff --git a/webui/index.html b/webui/index.html
index a8427378..4087a6db 100644
--- a/webui/index.html
+++ b/webui/index.html
@@ -5794,9 +5794,15 @@
+
+
+
diff --git a/webui/static/script.js b/webui/static/script.js
index 7e9e75fc..b50e63b9 100644
--- a/webui/static/script.js
+++ b/webui/static/script.js
@@ -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 = `
@@ -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) {
diff --git a/webui/static/style.css b/webui/static/style.css
index cd25c0d2..f0119d0b 100644
--- a/webui/static/style.css
+++ b/webui/static/style.css
@@ -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);