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);