Add Select All, Fix Selected & Fix All to Library Maintenance findings
This commit is contained in:
parent
162542a0cf
commit
cc62541a65
5 changed files with 199 additions and 1 deletions
|
|
@ -1043,6 +1043,55 @@ class RepairWorker:
|
||||||
if conn:
|
if conn:
|
||||||
conn.close()
|
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:
|
def bulk_update_findings(self, finding_ids: List[int], action: str) -> int:
|
||||||
"""Bulk resolve or dismiss findings. Returns count updated."""
|
"""Bulk resolve or dismiss findings. Returns count updated."""
|
||||||
conn = None
|
conn = None
|
||||||
|
|
|
||||||
|
|
@ -39306,6 +39306,31 @@ def repair_finding_dismiss(finding_id):
|
||||||
logger.error(f"Error dismissing finding {finding_id}: {e}")
|
logger.error(f"Error dismissing finding {finding_id}: {e}")
|
||||||
return jsonify({'error': str(e)}), 500
|
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'])
|
@app.route('/api/repair/findings/bulk', methods=['POST'])
|
||||||
def repair_findings_bulk():
|
def repair_findings_bulk():
|
||||||
"""Bulk resolve or dismiss findings"""
|
"""Bulk resolve or dismiss findings"""
|
||||||
|
|
|
||||||
|
|
@ -5794,9 +5794,15 @@
|
||||||
<option value="dismissed">Dismissed</option>
|
<option value="dismissed">Dismissed</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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;">
|
<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 fix" onclick="bulkFixFindings()">Fix Selected</button>
|
||||||
<button class="repair-bulk-btn" onclick="bulkRepairAction('dismiss')">Dismiss 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>
|
</div>
|
||||||
<button class="repair-clear-btn" onclick="clearRepairFindings()" title="Clear findings matching current filters">Clear Findings</button>
|
<button class="repair-clear-btn" onclick="clearRepairFindings()" title="Clear findings matching current filters">Clear Findings</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -51607,6 +51607,7 @@ function updateRepairStatusFromData(data) {
|
||||||
let _repairCurrentTab = 'jobs';
|
let _repairCurrentTab = 'jobs';
|
||||||
let _repairFindingsPage = 0;
|
let _repairFindingsPage = 0;
|
||||||
let _repairSelectedFindings = new Set();
|
let _repairSelectedFindings = new Set();
|
||||||
|
let _repairFindingsTotal = 0;
|
||||||
const REPAIR_FINDINGS_PAGE_SIZE = 30;
|
const REPAIR_FINDINGS_PAGE_SIZE = 30;
|
||||||
let _repairJobsCache = {}; // Cache job data for help modal
|
let _repairJobsCache = {}; // Cache job data for help modal
|
||||||
|
|
||||||
|
|
@ -52154,8 +52155,11 @@ async function loadRepairFindings() {
|
||||||
const items = data.items || [];
|
const items = data.items || [];
|
||||||
|
|
||||||
_repairSelectedFindings.clear();
|
_repairSelectedFindings.clear();
|
||||||
|
_repairFindingsTotal = data.total || 0;
|
||||||
const bulkBar = document.getElementById('repair-findings-bulk');
|
const bulkBar = document.getElementById('repair-findings-bulk');
|
||||||
if (bulkBar) bulkBar.style.display = 'none';
|
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) {
|
if (items.length === 0) {
|
||||||
container.innerHTML = `<div class="repair-empty-state">
|
container.innerHTML = `<div class="repair-empty-state">
|
||||||
|
|
@ -52537,8 +52541,88 @@ function toggleFindingSelect(id, checked) {
|
||||||
if (checked) _repairSelectedFindings.add(id);
|
if (checked) _repairSelectedFindings.add(id);
|
||||||
else _repairSelectedFindings.delete(id);
|
else _repairSelectedFindings.delete(id);
|
||||||
|
|
||||||
|
_updateFindingsBulkBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _updateFindingsBulkBar() {
|
||||||
const bulkBar = document.getElementById('repair-findings-bulk');
|
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) {
|
function renderRepairFindingsPagination(total, currentPage) {
|
||||||
|
|
|
||||||
|
|
@ -41158,11 +41158,35 @@ tr.tag-diff-same {
|
||||||
font-weight: 600;
|
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 {
|
.repair-findings-bulk {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repair-bulk-count {
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.repair-bulk-btn {
|
.repair-bulk-btn {
|
||||||
background: rgba(255, 255, 255, 0.06);
|
background: rgba(255, 255, 255, 0.06);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
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);
|
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 {
|
.repair-clear-btn {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
background: rgba(239, 68, 68, 0.08);
|
background: rgba(239, 68, 68, 0.08);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue