From 4635ea895bcecd4bd288147becec5b0147e6aca9 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Sun, 15 Mar 2026 11:19:09 -0700 Subject: [PATCH] include a '?' to include more details for each job. --- core/repair_jobs/acoustid_scanner.py | 12 + core/repair_jobs/album_completeness.py | 10 + core/repair_jobs/base.py | 1 + core/repair_jobs/cache_evictor.py | 7 + core/repair_jobs/dead_file_cleaner.py | 8 + core/repair_jobs/duplicate_detector.py | 10 + core/repair_jobs/fake_lossless_detector.py | 11 + core/repair_jobs/library_reorganize.py | 13 + core/repair_jobs/metadata_gap_filler.py | 10 + core/repair_jobs/missing_cover_art.py | 9 + core/repair_jobs/orphan_file_detector.py | 8 + core/repair_jobs/track_number_repair.py | 11 + core/repair_worker.py | 1 + webui/index.html | 2 +- webui/static/script.js | 198 ++++++++-- webui/static/style.css | 427 +++++++++++++++------ 16 files changed, 589 insertions(+), 149 deletions(-) diff --git a/core/repair_jobs/acoustid_scanner.py b/core/repair_jobs/acoustid_scanner.py index 4ac62679..4ca8b9c5 100644 --- a/core/repair_jobs/acoustid_scanner.py +++ b/core/repair_jobs/acoustid_scanner.py @@ -20,6 +20,18 @@ class AcoustIDScannerJob(RepairJob): job_id = 'acoustid_scanner' display_name = 'AcoustID Scanner' description = 'Fingerprints tracks to detect wrong downloads' + help_text = ( + 'Generates audio fingerprints using the AcoustID/Chromaprint service and compares ' + 'the identified recording against what you expected to download. This catches cases ' + 'where the wrong song was served — even if the filename looks correct.\n\n' + 'The job processes tracks in batches and saves a checkpoint so it can resume where ' + 'it left off across runs. Requires an AcoustID API key (set in Settings).\n\n' + 'Settings:\n' + '- Fingerprint Threshold: Minimum AcoustID match confidence (0.0 - 1.0)\n' + '- Title Similarity: How closely the identified title must match your expected title\n' + '- Artist Similarity: How closely the identified artist must match\n' + '- Batch Size: Number of tracks to process per scan run' + ) icon = 'repair-icon-acoustid' default_enabled = False default_interval_hours = 168 diff --git a/core/repair_jobs/album_completeness.py b/core/repair_jobs/album_completeness.py index 06886b12..cd857ddf 100644 --- a/core/repair_jobs/album_completeness.py +++ b/core/repair_jobs/album_completeness.py @@ -12,6 +12,16 @@ class AlbumCompletenessJob(RepairJob): job_id = 'album_completeness' display_name = 'Album Completeness' description = 'Checks if all tracks from albums are present' + help_text = ( + 'Compares the number of tracks you have for each album against the expected total ' + 'from the Spotify tracklist. Albums where tracks are missing get flagged as findings ' + 'with details about which tracks are absent.\n\n' + 'Useful for catching partial downloads or albums where some tracks failed to download. ' + 'You can use the Download Missing feature from the album page to fill gaps.\n\n' + 'Settings:\n' + '- Min Tracks For Check: Only check albums with at least this many expected tracks ' + '(skips singles and EPs)' + ) icon = 'repair-icon-completeness' default_enabled = False default_interval_hours = 168 diff --git a/core/repair_jobs/base.py b/core/repair_jobs/base.py index eadf475c..ce35cd09 100644 --- a/core/repair_jobs/base.py +++ b/core/repair_jobs/base.py @@ -58,6 +58,7 @@ class RepairJob(ABC): job_id: str = '' display_name: str = '' description: str = '' + help_text: str = '' # Extended explanation shown in the info modal icon: str = '' default_enabled: bool = False default_interval_hours: int = 24 diff --git a/core/repair_jobs/cache_evictor.py b/core/repair_jobs/cache_evictor.py index 68e3c613..2bf179df 100644 --- a/core/repair_jobs/cache_evictor.py +++ b/core/repair_jobs/cache_evictor.py @@ -12,6 +12,13 @@ class CacheEvictorJob(RepairJob): job_id = 'cache_evictor' display_name = 'Cache Evictor' description = 'Removes expired metadata cache entries' + help_text = ( + 'Automatically cleans up stale metadata cache entries from the database. ' + 'SoulSync caches search results, album metadata, and cover art URLs to avoid ' + 'hitting API rate limits. Over time these entries expire and take up space.\n\n' + 'This is the only fully automatic job — it runs silently and removes expired ' + 'entries without creating findings. Safe to leave enabled at all times.' + ) icon = 'repair-icon-cache' default_enabled = True default_interval_hours = 6 diff --git a/core/repair_jobs/dead_file_cleaner.py b/core/repair_jobs/dead_file_cleaner.py index 60d8c5e2..ac0e8fe4 100644 --- a/core/repair_jobs/dead_file_cleaner.py +++ b/core/repair_jobs/dead_file_cleaner.py @@ -14,6 +14,14 @@ class DeadFileCleanerJob(RepairJob): job_id = 'dead_file_cleaner' display_name = 'Dead File Cleaner' description = 'Finds database entries pointing to missing files' + help_text = ( + 'Checks every track in your database to verify the actual audio file still exists ' + 'on disk. If a file has been moved, renamed, or deleted outside of SoulSync, the ' + 'database entry becomes a "dead" reference.\n\n' + 'Each dead reference is reported as a finding. You can then resolve it by re-downloading ' + 'the track or dismiss it to clean up the database entry.\n\n' + 'This job only scans and reports — it never deletes database entries automatically.' + ) icon = 'repair-icon-deadfile' default_enabled = True default_interval_hours = 24 diff --git a/core/repair_jobs/duplicate_detector.py b/core/repair_jobs/duplicate_detector.py index d558092d..70e754fe 100644 --- a/core/repair_jobs/duplicate_detector.py +++ b/core/repair_jobs/duplicate_detector.py @@ -16,6 +16,16 @@ class DuplicateDetectorJob(RepairJob): job_id = 'duplicate_detector' display_name = 'Duplicate Detector' description = 'Finds potential duplicate tracks in your library' + help_text = ( + 'Groups tracks by similar title and artist name using fuzzy matching, then flags ' + 'groups where multiple copies exist. This helps you find accidental duplicates ' + 'from re-downloads, compilation albums, or similar-titled tracks.\n\n' + 'Each duplicate group is reported as a finding with details about every copy ' + '(file path, format, bitrate) so you can decide which to keep.\n\n' + 'Settings:\n' + '- Title Similarity: How closely titles must match to be considered duplicates (0.0 - 1.0)\n' + '- Artist Similarity: How closely artist names must match (0.0 - 1.0)' + ) icon = 'repair-icon-duplicate' default_enabled = False default_interval_hours = 168 diff --git a/core/repair_jobs/fake_lossless_detector.py b/core/repair_jobs/fake_lossless_detector.py index ec5121c3..93e81794 100644 --- a/core/repair_jobs/fake_lossless_detector.py +++ b/core/repair_jobs/fake_lossless_detector.py @@ -19,6 +19,17 @@ class FakeLosslessDetectorJob(RepairJob): job_id = 'fake_lossless_detector' display_name = 'Fake Lossless Detector' description = 'Detects FLAC/WAV files likely transcoded from lossy' + help_text = ( + 'Analyzes the spectral content of FLAC and WAV files to detect if they were ' + 'transcoded from a lossy source (like MP3 or AAC). A genuine lossless file has ' + 'audio content extending to 20kHz+, while a transcoded file shows a sharp ' + 'frequency cutoff around 16-18kHz.\n\n' + 'Files flagged as fake lossless are reported as findings. You may want to ' + 're-download them from a better source or keep the lossy version.\n\n' + 'Settings:\n' + '- Spectral Cutoff kHz: Frequency threshold below which a file is considered ' + 'suspicious (default 16.0 kHz)' + ) icon = 'repair-icon-lossless' default_enabled = False default_interval_hours = 168 diff --git a/core/repair_jobs/library_reorganize.py b/core/repair_jobs/library_reorganize.py index 52c1c56e..47870ae4 100644 --- a/core/repair_jobs/library_reorganize.py +++ b/core/repair_jobs/library_reorganize.py @@ -225,6 +225,19 @@ class LibraryReorganizeJob(RepairJob): job_id = 'library_reorganize' display_name = 'Library Reorganize' description = 'Moves files to match the current file organization template (dry run by default)' + help_text = ( + 'Scans your transfer folder and reads each audio file\'s tags (artist, album, title, ' + 'track number, disc number) to compute the expected file path based on your current ' + 'file organization template from Settings.\n\n' + 'Any file whose actual path doesn\'t match the expected template gets flagged. In dry ' + 'run mode (default), a finding is created showing the current and expected paths. ' + 'Disable dry run to have the job move files automatically.\n\n' + 'Safety features: case-insensitive path comparison on Windows/macOS, collision ' + 'detection, path escape prevention, and sidecar file handling (.lrc, .nfo, etc.).\n\n' + 'Settings:\n' + '- Dry Run: When enabled, only reports what would change without moving files\n' + '- Move Sidecars: Also move associated files (.lrc, .jpg, .nfo) alongside audio files' + ) icon = 'repair-icon-reorganize' default_enabled = False default_interval_hours = 168 # Weekly — but disabled by default so won't auto-run diff --git a/core/repair_jobs/metadata_gap_filler.py b/core/repair_jobs/metadata_gap_filler.py index 2b186a0f..606f3581 100644 --- a/core/repair_jobs/metadata_gap_filler.py +++ b/core/repair_jobs/metadata_gap_filler.py @@ -14,6 +14,16 @@ class MetadataGapFillerJob(RepairJob): job_id = 'metadata_gap_filler' display_name = 'Metadata Gap Filler' description = 'Finds tracks missing ISRC or MusicBrainz IDs and locates them' + help_text = ( + 'Searches for tracks in your library that are missing important metadata identifiers: ' + 'ISRC codes and MusicBrainz recording IDs. These identifiers are used for accurate ' + 'matching, scrobbling, and enrichment.\n\n' + 'For each track with gaps, the job queries MusicBrainz by title and artist to find ' + 'the correct IDs. Results are reported as findings for your review.\n\n' + 'Settings:\n' + '- Fill ISRC: Look up missing ISRC codes\n' + '- Fill MusicBrainz ID: Look up missing MusicBrainz recording IDs' + ) icon = 'repair-icon-metadata' default_enabled = False default_interval_hours = 72 diff --git a/core/repair_jobs/missing_cover_art.py b/core/repair_jobs/missing_cover_art.py index 6dffdbfa..bc8888db 100644 --- a/core/repair_jobs/missing_cover_art.py +++ b/core/repair_jobs/missing_cover_art.py @@ -12,6 +12,15 @@ class MissingCoverArtJob(RepairJob): job_id = 'missing_cover_art' display_name = 'Cover Art Filler' description = 'Finds albums missing artwork and locates art from Spotify/iTunes' + help_text = ( + 'Scans your library for albums that have no cover art stored in the database. ' + 'For each missing cover, it searches Spotify and iTunes APIs using the album name ' + 'and artist to find matching artwork.\n\n' + 'When artwork is found, a finding is created with the image URL so you can review ' + 'and apply it. The job does not download or embed artwork automatically.\n\n' + 'Settings:\n' + '- Prefer Source: Which API to try first for artwork (spotify or itunes)' + ) icon = 'repair-icon-coverart' default_enabled = True default_interval_hours = 48 diff --git a/core/repair_jobs/orphan_file_detector.py b/core/repair_jobs/orphan_file_detector.py index 6ec61669..9213ca77 100644 --- a/core/repair_jobs/orphan_file_detector.py +++ b/core/repair_jobs/orphan_file_detector.py @@ -17,6 +17,14 @@ class OrphanFileDetectorJob(RepairJob): job_id = 'orphan_file_detector' display_name = 'Orphan File Detector' description = 'Finds audio files not tracked in the database' + help_text = ( + 'Walks your transfer folder looking for audio files (FLAC, MP3, M4A, OGG, WAV, etc.) ' + 'that exist on disk but have no matching entry in the SoulSync database.\n\n' + 'Orphan files can appear after manual folder edits, interrupted downloads, or database ' + 'issues. Each orphan is reported as a finding so you can decide whether to import it ' + 'into your library or remove it.\n\n' + 'This job only scans and reports — it never moves or deletes files on its own.' + ) icon = 'repair-icon-orphan' default_enabled = True default_interval_hours = 24 diff --git a/core/repair_jobs/track_number_repair.py b/core/repair_jobs/track_number_repair.py index 8c152434..10d0d201 100644 --- a/core/repair_jobs/track_number_repair.py +++ b/core/repair_jobs/track_number_repair.py @@ -30,6 +30,17 @@ class TrackNumberRepairJob(RepairJob): job_id = 'track_number_repair' display_name = 'Track Number Repair' description = 'Detects mismatched track numbers using API lookups (dry run by default)' + help_text = ( + 'Scans album folders and compares each file\'s track number against the correct ' + 'tracklist from Spotify or iTunes. If a file\'s embedded track number doesn\'t match ' + 'the API data, the job creates a finding showing what needs to change.\n\n' + 'In dry run mode (default), no files are modified — you review each proposed change ' + 'in the Findings tab and decide what to approve. Disable dry run in settings to let ' + 'the job automatically rename and re-number files.\n\n' + 'Settings:\n' + '- Title Similarity: How closely a filename must match the API track title (0.0 - 1.0)\n' + '- Dry Run: When enabled, only reports issues without modifying files' + ) icon = 'repair-icon-tracknumber' default_enabled = True default_interval_hours = 24 diff --git a/core/repair_worker.py b/core/repair_worker.py index 0984301b..fb77d755 100644 --- a/core/repair_worker.py +++ b/core/repair_worker.py @@ -231,6 +231,7 @@ class RepairWorker: 'job_id': job_id, 'display_name': job.display_name, 'description': job.description, + 'help_text': job.help_text, 'icon': job.icon, 'auto_fix': job.auto_fix, 'enabled': config['enabled'], diff --git a/webui/index.html b/webui/index.html index bdb836bc..fa413569 100644 --- a/webui/index.html +++ b/webui/index.html @@ -5617,7 +5617,7 @@ diff --git a/webui/static/script.js b/webui/static/script.js index cba22dde..9eaca270 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -49857,6 +49857,7 @@ let _repairCurrentTab = 'jobs'; let _repairFindingsPage = 0; let _repairSelectedFindings = new Set(); const REPAIR_FINDINGS_PAGE_SIZE = 30; +let _repairJobsCache = {}; // Cache job data for help modal /** * Open the Library Maintenance modal @@ -49932,8 +49933,16 @@ async function loadRepairJobs() { const data = await response.json(); const jobs = data.jobs || []; + // Cache job data for help modal + _repairJobsCache = {}; + jobs.forEach(j => { _repairJobsCache[j.job_id] = j; }); + if (jobs.length === 0) { - container.innerHTML = '
No jobs available
'; + container.innerHTML = `
+
🔧
+
No Maintenance Jobs
+
Library maintenance jobs will appear here once available.
+
`; return; } @@ -49952,8 +49961,39 @@ async function loadRepairJobs() { const lastRunText = job.last_run ? formatCacheAge(job.last_run.finished_at) : 'Never'; const nextRunText = job.next_run ? formatCacheAge(job.next_run) : (job.enabled ? 'Pending' : '-'); const statusClass = job.is_running ? 'running' : (job.enabled ? 'idle' : 'disabled'); - const statusText = job.is_running ? 'Running' : (job.enabled ? 'Idle' : 'Disabled'); - const lastStats = job.last_run ? `Scanned: ${(job.last_run.items_scanned || 0).toLocaleString()}` : ''; + const dotClass = job.is_running ? 'running' : (job.enabled ? 'enabled' : 'disabled'); + const cardClass = job.is_running ? 'running' : (!job.enabled ? 'disabled' : ''); + + // Build flow badges + const flowParts = []; + flowParts.push(`${job.is_running ? '▶ Running' : 'Scan'}`); + if (job.auto_fix) { + flowParts.push(''); + const isDryRun = job.settings && job.settings.dry_run === true; + if (isDryRun) { + flowParts.push('Dry Run'); + } else { + flowParts.push('Auto-fix'); + } + } + // Show pending findings count + const findingsCount = job.last_run ? (job.last_run.findings_created || 0) : 0; + if (findingsCount > 0) { + flowParts.push(''); + flowParts.push(`${findingsCount} finding${findingsCount !== 1 ? 's' : ''}`); + } + + // Build meta parts + const metaParts = []; + metaParts.push('Last: ' + lastRunText); + metaParts.push('Next: ' + nextRunText); + if (job.last_run) { + metaParts.push(`Scanned: ${(job.last_run.items_scanned || 0).toLocaleString()}`); + if (job.last_run.auto_fixed) metaParts.push(`Fixed: ${job.last_run.auto_fixed}`); + } + if (job.last_run && job.last_run.duration_seconds) { + metaParts.push(`${job.last_run.duration_seconds.toFixed(1)}s`); + } // Build settings HTML let settingsHtml = ''; @@ -49986,20 +50026,13 @@ async function loadRepairJobs() { `; } - return `
+ return `
+
-
- - ${job.display_name} - ${statusText} - ${job.auto_fix ? 'Auto-fix' : ''} -
-
${job.description}
-
- Last: ${lastRunText} · Next: ${nextRunText} - ${lastStats ? ' · ' + lastStats : ''} -
+
${job.display_name}
+
${flowParts.join('')}
+
${metaParts.join(' · ')}
${settingsHtml} @@ -50031,6 +50066,13 @@ async function toggleRepairJob(jobId, enabled) { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled }) }); + // Update card visuals immediately + const card = document.querySelector(`.repair-job-card[data-job-id="${jobId}"]`); + if (card) { + card.classList.toggle('disabled', !enabled); + const dot = card.querySelector('.repair-job-status'); + if (dot) dot.className = 'repair-job-status ' + (enabled ? 'enabled' : 'disabled'); + } } catch (error) { console.error('Error toggling job:', error); showToast('Error toggling job', 'error'); @@ -50042,6 +50084,74 @@ function expandRepairJobSettings(jobId) { if (el) el.style.display = el.style.display === 'none' ? '' : 'none'; } +function showRepairJobHelp(jobId) { + const job = _repairJobsCache[jobId]; + if (!job) return; + + // Remove existing overlay if present + let overlay = document.getElementById('repair-help-overlay'); + if (overlay) overlay.remove(); + + // Build settings summary + let settingsHtml = ''; + if (job.settings && Object.keys(job.settings).length > 0) { + const rows = Object.entries(job.settings).map(([key, val]) => { + const label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); + const display = typeof val === 'boolean' ? (val ? 'Yes' : 'No') : val; + return `
${label}${display}
`; + }).join(''); + settingsHtml = `
+
Current Settings
+ ${rows} +
`; + } + + // Build info badges + const badges = []; + if (job.auto_fix) { + const isDryRun = job.settings && job.settings.dry_run === true; + badges.push(isDryRun + ? 'Dry Run' + : 'Auto-fix'); + } else { + badges.push('Scan Only'); + } + badges.push(`Every ${job.interval_hours}h`); + if (job.enabled) { + badges.push('Enabled'); + } else { + badges.push('Disabled'); + } + + // Format help text paragraphs + const helpBody = (job.help_text || job.description || '').split('\n\n').map(p => { + if (p.startsWith('Settings:\n')) { + const lines = p.split('\n').slice(1); + return '
' + + lines.map(l => `
${l.replace(/^- /, '')}
`).join('') + + '
'; + } + return `

${p.replace(/\n/g, '
')}

`; + }).join(''); + + overlay = document.createElement('div'); + overlay.id = 'repair-help-overlay'; + overlay.className = 'repair-help-overlay'; + overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); }; + overlay.innerHTML = ` +
+
+

${job.display_name}

+ +
+
${badges.join('')}
+
${helpBody}
+ ${settingsHtml} +
+ `; + document.body.appendChild(overlay); +} + async function saveRepairJobSettings(jobId) { try { const inputs = document.querySelectorAll(`.repair-setting-input[data-job="${jobId}"]`); @@ -50092,28 +50202,36 @@ function updateRepairJobProgressFromData(data) { const card = document.querySelector(`.repair-job-card[data-job-id="${jobId}"]`); if (!card) continue; - // Update status pill - const pill = card.querySelector('.repair-job-status-pill'); - if (pill) { - pill.className = 'repair-job-status-pill ' + state.status; - if (state.status === 'running') pill.textContent = 'Running'; - else if (state.status === 'finished') pill.textContent = 'Complete'; - else if (state.status === 'error') pill.textContent = 'Error'; + // Update status dot + const statusDot = card.querySelector('.repair-job-status'); + if (statusDot) { + if (state.status === 'running') statusDot.className = 'repair-job-status running'; + else if (state.status === 'finished') statusDot.className = 'repair-job-status enabled'; + else if (state.status === 'error') statusDot.className = 'repair-job-status enabled'; + } + + // Update flow badge to show running state + const firstBadge = card.querySelector('.repair-flow-badge.scan'); + if (firstBadge) { + if (state.status === 'running') firstBadge.innerHTML = '▶ Running'; + else if (state.status === 'finished') firstBadge.innerHTML = '✓ Complete'; + else if (state.status === 'error') firstBadge.innerHTML = '✗ Error'; } // Add/update card running class card.classList.toggle('running', state.status === 'running'); + card.classList.remove('disabled'); - // Create or find progress panel + // Create or find progress panel (bar-first layout like automation) let panel = card.querySelector('.repair-job-progress'); if (!panel) { panel = document.createElement('div'); panel.className = 'repair-job-progress'; panel.innerHTML = ` -
+
`; card.appendChild(panel); @@ -50124,14 +50242,28 @@ function updateRepairJobProgressFromData(data) { panel.classList.toggle('finished', state.status === 'finished'); panel.classList.toggle('error', state.status === 'error'); - // Update phase - const phaseEl = panel.querySelector('.repair-progress-phase'); - if (phaseEl && state.phase) phaseEl.textContent = state.phase; + if (state.status === 'running') { + panel.classList.remove('finished', 'error'); + if (_repairProgressHideTimers[jobId]) { + clearTimeout(_repairProgressHideTimers[jobId]); + delete _repairProgressHideTimers[jobId]; + } + // Reset log for re-run + if (_repairProgressLogCounts[jobId] > 0 && state.log && state.log.length < _repairProgressLogCounts[jobId]) { + const existingLog = panel.querySelector('.repair-progress-log'); + if (existingLog) existingLog.innerHTML = ''; + _repairProgressLogCounts[jobId] = 0; + } + } // Update progress bar const bar = panel.querySelector('.repair-progress-bar'); if (bar) bar.style.width = (state.progress || 0) + '%'; + // Update phase + const phaseEl = panel.querySelector('.repair-progress-phase'); + if (phaseEl && state.phase) phaseEl.textContent = state.phase; + // Update log const logEl = panel.querySelector('.repair-progress-log'); if (logEl && state.log) { @@ -50197,7 +50329,11 @@ async function loadRepairFindings() { if (bulkBar) bulkBar.style.display = 'none'; if (items.length === 0) { - container.innerHTML = '
No findings match your filters
'; + container.innerHTML = `
+
+
All Clear
+
No findings match your filters. Your library is looking good!
+
`; document.getElementById('repair-findings-pagination').innerHTML = ''; return; } @@ -50321,7 +50457,11 @@ async function loadRepairHistory() { const runs = data.runs || []; if (runs.length === 0) { - container.innerHTML = '
No job history yet
'; + container.innerHTML = `
+
🕑
+
No History Yet
+
Job run history will appear here after maintenance jobs complete their first scan.
+
`; return; } diff --git a/webui/static/style.css b/webui/static/style.css index c1078023..35d8500e 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -39814,30 +39814,7 @@ tr.tag-diff-same { transform: translateX(20px); } -.repair-toggle-slider.small { - width: 36px; - height: 20px; - border-radius: 10px; -} - -.repair-toggle-slider.small::after { - top: 2px; - left: 2px; - width: 16px; - height: 16px; -} - -.repair-job-toggle input:checked + .repair-toggle-slider.small::after { - transform: translateX(16px); -} - -.repair-job-toggle input { - display: none; -} - -.repair-job-toggle input:checked + .repair-toggle-slider.small { - background: var(--accent-color, #6366f1); -} +/* Job toggle sizing moved to .repair-job-toggle block in Jobs Tab section */ /* Tabs */ .repair-tabs { @@ -39899,6 +39876,25 @@ tr.tag-diff-same { font-size: 14px; } +/* Gradient empty state — matches automations-empty */ +.repair-empty-state { + text-align: center; + padding: 80px 24px; + background: linear-gradient(135deg, rgba(var(--accent-rgb, 99, 102, 241), 0.03) 0%, transparent 60%); + border-radius: 20px; + border: 1px dashed rgba(255, 255, 255, 0.08); +} +.repair-empty-icon { font-size: 56px; margin-bottom: 20px; filter: grayscale(0.3); } +.repair-empty-title { + font-size: 22px; font-weight: 700; margin-bottom: 10px; + background: linear-gradient(135deg, #fff 0%, rgb(var(--accent-light-rgb, 129, 140, 248)) 100%); + -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; +} +.repair-empty-text { + font-size: 14px; color: rgba(255, 255, 255, 0.4); margin-bottom: 0; + max-width: 420px; margin-left: auto; margin-right: auto; line-height: 1.6; +} + /* ── Jobs Tab ── */ .repair-jobs-list { display: flex; @@ -39907,118 +39903,180 @@ tr.tag-diff-same { } .repair-job-card { - background: rgba(22, 22, 22, 0.8); - border: 1px solid rgba(255, 255, 255, 0.06); + background: rgba(22, 22, 22, 0.95); + border: 1px solid rgba(255, 255, 255, 0.07); border-radius: 10px; overflow: hidden; - transition: border-color 0.2s ease; + transition: all 0.2s ease; } .repair-job-card:hover { - border-color: rgba(255, 255, 255, 0.12); + background: rgba(28, 28, 28, 0.98); + border-color: rgba(var(--accent-rgb, 99, 102, 241), 0.2); } .repair-job-card.running { - border-color: rgba(var(--accent-rgb, 99, 102, 241), 0.3); + border-color: rgba(var(--accent-rgb, 99, 102, 241), 0.35); + box-shadow: 0 0 12px rgba(var(--accent-rgb, 99, 102, 241), 0.08), + inset 0 0 20px rgba(var(--accent-rgb, 99, 102, 241), 0.03); +} + +.repair-job-card.disabled { + opacity: 0.55; +} +.repair-job-card.disabled:hover { + opacity: 0.75; } .repair-job-main { display: flex; - align-items: flex-start; - justify-content: space-between; - padding: 14px 16px; - gap: 16px; + align-items: center; + padding: 10px 14px; + gap: 10px; +} + +/* Status dot — matches automation-status */ +.repair-job-status { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} +.repair-job-status.enabled { + background: #4ade80; + box-shadow: 0 0 6px rgba(74, 222, 128, 0.4); +} +.repair-job-status.disabled { + background: #555; +} +.repair-job-status.running { + background: rgb(var(--accent-rgb, 99, 102, 241)); + box-shadow: 0 0 6px rgba(var(--accent-rgb, 99, 102, 241), 0.4); + animation: repair-status-pulse 1.5s ease-in-out infinite; +} +@keyframes repair-status-pulse { + 0%, 100% { opacity: 1; box-shadow: 0 0 6px rgba(var(--accent-rgb, 99, 102, 241), 0.4); } + 50% { opacity: 0.4; box-shadow: 0 0 2px rgba(var(--accent-rgb, 99, 102, 241), 0.2); } } .repair-job-info { flex: 1; min-width: 0; -} - -.repair-job-title { display: flex; - align-items: center; - gap: 8px; - margin-bottom: 4px; - flex-wrap: wrap; + flex-direction: column; + gap: 2px; } .repair-job-name { - font-size: 14px; + font-size: 13px; font-weight: 600; color: #fff; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } -.repair-job-status-pill { - font-size: 10px; - font-weight: 600; +/* Flow visualization — job type badges */ +.repair-job-flow { + display: flex; + gap: 6px; + flex-wrap: wrap; + align-items: center; +} + +.repair-flow-badge { + font-size: 11px; padding: 2px 8px; border-radius: 10px; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.repair-job-status-pill.running { - background: rgba(var(--accent-rgb, 99, 102, 241), 0.15); - color: var(--accent-color, #6366f1); -} - -.repair-job-status-pill.idle { - background: rgba(34, 197, 94, 0.12); - color: #22c55e; -} - -.repair-job-status-pill.disabled { - background: rgba(255, 255, 255, 0.06); - color: rgba(255, 255, 255, 0.35); -} - -.repair-job-autofix-pill { - font-size: 10px; font-weight: 500; - padding: 2px 6px; - border-radius: 8px; + white-space: nowrap; +} + +.repair-flow-badge.scan { + background: rgba(var(--accent-rgb, 99, 102, 241), 0.12); + color: rgb(var(--accent-light-rgb, 129, 140, 248)); +} +.repair-flow-badge.autofix { background: rgba(59, 130, 246, 0.12); color: #60a5fa; } - -.repair-job-desc { +.repair-flow-badge.dryrun { + background: rgba(250, 204, 21, 0.10); + color: #fbbf24; +} +.repair-flow-badge.findings { + background: rgba(239, 68, 68, 0.10); + color: #f87171; +} +.repair-flow-arrow { + color: rgba(255, 255, 255, 0.25); font-size: 12px; - color: rgba(255, 255, 255, 0.45); - margin-bottom: 4px; } .repair-job-meta { - font-size: 11px; + font-size: 10px; color: rgba(255, 255, 255, 0.3); + line-height: 1.3; } .repair-job-actions { display: flex; align-items: center; - gap: 8px; + gap: 4px; flex-shrink: 0; } +/* Toggle — matches automation-toggle (32x18) */ +.repair-job-toggle { + position: relative; + width: 32px; + height: 18px; + flex-shrink: 0; +} +.repair-job-toggle input { display: none; } +.repair-job-toggle .repair-toggle-slider.small { + width: 32px; + height: 18px; + border-radius: 9px; +} +.repair-job-toggle .repair-toggle-slider.small::after { + top: 3px; + left: 3px; + width: 12px; + height: 12px; +} +.repair-job-toggle input:checked + .repair-toggle-slider.small { + background: rgba(var(--accent-rgb, 99, 102, 241), 0.5); +} +.repair-job-toggle input:checked + .repair-toggle-slider.small::after { + transform: translateX(14px); + background: rgb(var(--accent-light-rgb, 129, 140, 248)); +} + +/* Action buttons — compact 26x26 */ .repair-run-btn, .repair-settings-btn { - background: rgba(255, 255, 255, 0.06); + background: transparent; border: 1px solid rgba(255, 255, 255, 0.08); - color: rgba(255, 255, 255, 0.5); - width: 30px; - height: 30px; + color: rgba(255, 255, 255, 0.4); + width: 26px; + height: 26px; border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center; - font-size: 12px; + font-size: 11px; transition: all 0.2s ease; } -.repair-run-btn:hover, .repair-settings-btn:hover { - background: rgba(255, 255, 255, 0.12); - color: #fff; - border-color: rgba(255, 255, 255, 0.15); +.repair-run-btn:hover { + background: rgba(var(--accent-rgb, 99, 102, 241), 0.15); + color: rgb(var(--accent-light-rgb, 129, 140, 248)); + border-color: rgba(var(--accent-rgb, 99, 102, 241), 0.3); +} +.repair-settings-btn:hover { + background: rgba(255, 255, 255, 0.08); + color: rgba(255, 255, 255, 0.7); } /* Job settings panel */ @@ -40373,64 +40431,35 @@ tr.tag-diff-same { } /* ── Repair Job Live Progress Panel ── */ -.repair-job-card.running { - border-color: rgba(var(--accent-rgb, 99, 102, 241), 0.35); - box-shadow: 0 0 12px rgba(var(--accent-rgb, 99, 102, 241), 0.1); -} - -@keyframes repair-card-pulse { +@keyframes repair-card-glow { 0%, 100% { box-shadow: 0 0 8px rgba(var(--accent-rgb, 99, 102, 241), 0.08); } - 50% { box-shadow: 0 0 16px rgba(var(--accent-rgb, 99, 102, 241), 0.18); } + 50% { box-shadow: 0 0 16px rgba(var(--accent-rgb, 99, 102, 241), 0.2); } } .repair-job-card.running { - animation: repair-card-pulse 2s ease-in-out infinite; -} - -.repair-job-status-pill.finished { - background: rgba(34, 197, 94, 0.12); - color: #22c55e; -} - -.repair-job-status-pill.error { - background: rgba(239, 68, 68, 0.12); - color: #ef4444; -} - -.repair-job-icon { - display: flex; - align-items: center; - font-size: 14px; - color: rgba(255, 255, 255, 0.4); + animation: repair-card-glow 2s ease-in-out infinite; } .repair-job-progress { max-height: 0; overflow: hidden; transition: max-height 0.3s ease, padding 0.3s ease; - padding: 0 16px; + padding: 0 14px; } .repair-job-progress.visible { max-height: 300px; - padding: 10px 16px 14px; + padding: 8px 14px 12px; border-top: 1px solid rgba(255, 255, 255, 0.04); } -.repair-progress-phase { - font-size: 11px; - color: rgba(255, 255, 255, 0.55); - margin-bottom: 6px; - font-weight: 500; -} - .repair-progress-bar-wrap { width: 100%; - height: 4px; + height: 3px; background: rgba(255, 255, 255, 0.06); border-radius: 2px; overflow: hidden; - margin-bottom: 8px; + margin-bottom: 6px; } .repair-progress-bar { @@ -40448,8 +40477,17 @@ tr.tag-diff-same { background: #ef4444; } +.repair-progress-phase { + font-size: 10px; + color: rgba(255, 255, 255, 0.5); + margin-bottom: 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + .repair-progress-log { - max-height: 160px; + max-height: 200px; overflow-y: auto; font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace; font-size: 10px; @@ -40481,3 +40519,154 @@ tr.tag-diff-same { .repair-log-line.skip { color: rgba(255, 255, 255, 0.25); } + +/* ── Repair Job Help Button & Modal ── */ +.repair-help-btn { + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.08); + color: rgba(255, 255, 255, 0.35); + width: 26px; + height: 26px; + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 600; + transition: all 0.2s ease; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; +} +.repair-help-btn:hover { + background: rgba(var(--accent-rgb, 99, 102, 241), 0.15); + color: rgb(var(--accent-light-rgb, 129, 140, 248)); + border-color: rgba(var(--accent-rgb, 99, 102, 241), 0.3); +} + +.repair-help-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.6); + z-index: 10001; + display: flex; + align-items: center; + justify-content: center; + backdrop-filter: blur(4px); + animation: repair-help-fadein 0.15s ease; +} +@keyframes repair-help-fadein { + from { opacity: 0; } + to { opacity: 1; } +} + +.repair-help-modal { + background: rgba(18, 18, 18, 0.98); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 16px; + width: 90vw; + max-width: 480px; + max-height: 80vh; + overflow-y: auto; + box-shadow: 0 24px 64px rgba(0, 0, 0, 0.6); + animation: repair-help-slidein 0.2s ease; +} +@keyframes repair-help-slidein { + from { transform: translateY(12px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} + +.repair-help-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 24px 12px; +} +.repair-help-header h3 { + margin: 0; + font-size: 18px; + font-weight: 600; + color: #fff; +} +.repair-help-close { + background: none; + border: none; + color: rgba(255, 255, 255, 0.4); + font-size: 22px; + cursor: pointer; + padding: 4px 8px; + border-radius: 6px; + transition: all 0.2s ease; +} +.repair-help-close:hover { + color: #fff; + background: rgba(255, 255, 255, 0.1); +} + +.repair-help-badges { + display: flex; + gap: 6px; + flex-wrap: wrap; + padding: 0 24px 16px; +} + +.repair-help-body { + padding: 0 24px 20px; + color: rgba(255, 255, 255, 0.65); + font-size: 13px; + line-height: 1.7; +} +.repair-help-body p { + margin: 0 0 12px; +} +.repair-help-body p:last-child { + margin-bottom: 0; +} + +.repair-help-setting-list { + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 8px; + padding: 10px 14px; + margin-top: 4px; +} +.repair-help-setting-item { + font-size: 12px; + color: rgba(255, 255, 255, 0.55); + padding: 3px 0; + line-height: 1.5; +} +.repair-help-setting-item::before { + content: '\2022'; + color: rgb(var(--accent-light-rgb, 129, 140, 248)); + margin-right: 8px; +} + +.repair-help-settings-section { + padding: 0 24px 20px; +} +.repair-help-section-title { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: rgba(255, 255, 255, 0.35); + margin-bottom: 8px; +} +.repair-help-setting { + display: flex; + justify-content: space-between; + padding: 5px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.04); +} +.repair-help-setting:last-child { + border-bottom: none; +} +.repair-help-setting-key { + font-size: 12px; + color: rgba(255, 255, 255, 0.5); +} +.repair-help-setting-val { + font-size: 12px; + color: #fff; + font-weight: 500; +}