From d9b4e5b85324512e13c6f0a46b8e883d1edf3279 Mon Sep 17 00:00:00 2001
From: Broque Thomas <26755000+Nezreka@users.noreply.github.com>
Date: Thu, 16 Apr 2026 11:45:29 -0700
Subject: [PATCH] Add smart Library Status card to Dashboard with deep scan
support
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Adaptive card on the Dashboard showing library state with four modes:
- No server: gold accent, directs to Settings
- Disconnected: gold warning with troubleshooting guidance
- Empty library: blue accent with prominent Scan Now button
- Healthy: green accent with stats grid (artists/albums/tracks/DB size),
Refresh button (incremental) and Deep Scan button (full re-check)
Stats displayed as mini cards with individual icons. Animated glow orb,
gradient accent top line, shimmer progress bar during scans. Deep scan
added to /api/database/update endpoint (deep_scan flag) — re-checks
every track, adds new ones, removes stale, preserves enrichment data.
Confirmation dialog explains what deep scan does before starting.
---
web_server.py | 18 +-
webui/index.html | 77 +++++++++
webui/static/helper.js | 2 +
webui/static/script.js | 323 +++++++++++++++++++++++++++++++++-
webui/static/style.css | 381 +++++++++++++++++++++++++++++++++++++++++
5 files changed, 791 insertions(+), 10 deletions(-)
diff --git a/web_server.py b/web_server.py
index 86ec5c04..dd6a0db1 100644
--- a/web_server.py
+++ b/web_server.py
@@ -25163,21 +25163,25 @@ def start_database_update():
data = request.get_json()
full_refresh = data.get('full_refresh', False)
+ deep_scan = data.get('deep_scan', False)
active_server = config_manager.get_active_media_server()
+ scan_type = "Deep scan" if deep_scan else ("Full" if full_refresh else "Incremental")
db_update_state.update({
"status": "running",
- "phase": "Initializing...",
+ "phase": f"{scan_type}: Initializing...",
"progress": 0, "current_item": "", "processed": 0, "total": 0, "error_message": ""
})
-
+
# Add activity for database update start
- update_type = "Full" if full_refresh else "Incremental"
server_name = active_server.capitalize()
- add_activity_item("", "Database Update", f"Starting {update_type.lower()} update from {server_name}...", "Now")
-
- # Submit the worker function to the executor
- db_update_executor.submit(_run_db_update_task, full_refresh, active_server)
+ add_activity_item("", "Database Update", f"Starting {scan_type.lower()} update from {server_name}...", "Now")
+
+ # Submit the appropriate worker
+ if deep_scan:
+ db_update_executor.submit(_run_deep_scan_task, active_server)
+ else:
+ db_update_executor.submit(_run_db_update_task, full_refresh, active_server)
return jsonify({"success": True, "message": "Database update started."})
diff --git a/webui/index.html b/webui/index.html
index cde26366..cfacb79d 100644
--- a/webui/index.html
+++ b/webui/index.html
@@ -680,6 +680,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
Scanning...
+
+
0 / 0
+
+
+
+
+
+
Recent Syncs
diff --git a/webui/static/helper.js b/webui/static/helper.js
index 964ebd93..889479b5 100644
--- a/webui/static/helper.js
+++ b/webui/static/helper.js
@@ -3602,6 +3602,8 @@ const WHATS_NEW = {
'2.2': [
// --- April 15, 2026 ---
{ date: 'April 15, 2026' },
+ { title: 'Dashboard Library Status Card', desc: 'Smart card on the Dashboard showing your library state — server connection, track counts, last refresh time. Guides new users through setup, shows empty-library prompts, and lets you trigger a scan directly from the dashboard', page: 'dashboard' },
+ { title: 'AcoustID Scanner Upgrade', desc: 'Now scans your full library (not just Transfer) to detect wrong downloads. Actionable fixes: retag with correct metadata, re-download the right track, or delete the wrong file. Enabled by default, runs daily' },
{ title: 'Tools Page', desc: 'All tool cards (Database Updater, Quality Scanner, Duplicate Cleaner, Retag, Backups, Cache, etc.) and Library Maintenance moved from the Dashboard to a dedicated Tools page in the sidebar. Dashboard shows a quick-link card', page: 'tools' },
{ title: 'Watchlist & Wishlist Sidebar Pages', desc: 'Watchlist and Wishlist promoted from modals to full sidebar pages. All features preserved — artist grid, scan controls, batch operations, live activity, countdown timers, category cards with mosaic backgrounds. Header buttons now navigate to the pages', page: 'watchlist' },
{ title: 'Picard-Style MusicBrainz Album Consistency', desc: 'Recording MBIDs now pulled from the matched release tracklist instead of independent searches. Batch-level artist name used for stable cache keys. Post-batch consistency pass rewrites album-level tags on all files to guarantee identical MusicBrainz IDs — prevents Navidrome album splits' },
diff --git a/webui/static/script.js b/webui/static/script.js
index f5069d10..804297bd 100644
--- a/webui/static/script.js
+++ b/webui/static/script.js
@@ -381,6 +381,9 @@ function initializeWebSocket() {
}
function handleServiceStatusUpdate(data) {
+ // Cache for library status card
+ _lastServiceStatus = data;
+
// Same logic as fetchAndUpdateServiceStatus response handler
updateServiceStatus('spotify', data.spotify);
updateServiceStatus('media-server', data.media_server);
@@ -25173,9 +25176,10 @@ async function loadDashboardData() {
stopWishlistCountPolling(); // Ensure no duplicates
wishlistCountInterval = setInterval(updateWishlistCount, 10000);
- // Initial load of service status and system statistics
+ // Initial load of service status, system statistics, and library status
await fetchAndUpdateServiceStatus();
await fetchAndUpdateSystemStats();
+ await fetchAndUpdateDbStats();
// Service status is already polled globally (line 311)
// System stats polling kept here (dashboard-specific)
@@ -25221,8 +25225,318 @@ async function fetchAndUpdateDbStats() {
}
function updateDashboardStatCards(stats) {
- // You can expand this later to update the main stat cards
- // For now, we focus on the updater tool itself.
+ // Update the Library Status card on the dashboard
+ updateLibraryStatusCard(stats);
+}
+
+/**
+ * Smart Library Status card on the Dashboard.
+ * Shows different states: no server, empty library, healthy library, scanning.
+ */
+function updateLibraryStatusCard(dbStats) {
+ const card = document.getElementById('library-status-card');
+ if (!card) return;
+
+ const title = document.getElementById('library-status-title');
+ const subtitle = document.getElementById('library-status-subtitle');
+ const statsRow = document.getElementById('library-status-stats');
+ const scanBtn = document.getElementById('library-status-scan-btn');
+ const scanLabel = document.getElementById('library-status-scan-label');
+ const deepBtn = document.getElementById('library-status-deep-btn');
+ const progressDiv = document.getElementById('library-status-progress');
+ const messageDiv = document.getElementById('library-status-message');
+
+ const artists = dbStats ? (dbStats.artists || 0) : 0;
+ const albums = dbStats ? (dbStats.albums || 0) : 0;
+ const tracks = dbStats ? (dbStats.tracks || 0) : 0;
+ const sizeMb = dbStats ? (dbStats.database_size_mb || 0) : 0;
+ const lastUpdate = dbStats ? dbStats.last_update : null;
+ const serverSource = dbStats ? dbStats.server_source : null;
+
+ // Check if a scan is in progress
+ const isScanning = window._libraryStatusScanning || false;
+
+ // Determine state
+ const serverConnected = _lastServiceStatus && _lastServiceStatus.media_server && _lastServiceStatus.media_server.connected;
+ const serverType = _lastServiceStatus && _lastServiceStatus.active_media_server;
+ const hasData = tracks > 0;
+ const hasServer = !!serverType && serverType !== 'none';
+
+ // Reset classes
+ card.className = 'library-status-card';
+
+ if (isScanning) {
+ // State: Scanning
+ card.classList.add('scanning');
+ if (title) title.textContent = 'Library Scan';
+ if (subtitle) subtitle.textContent = 'Updating library database...';
+ if (scanBtn) {
+ scanBtn.style.display = '';
+ scanBtn.classList.add('scanning');
+ scanLabel.textContent = 'Stop';
+ scanBtn.disabled = false;
+ }
+ if (deepBtn) deepBtn.style.display = 'none';
+ if (statsRow) statsRow.style.display = hasData ? '' : 'none';
+ if (progressDiv) progressDiv.style.display = '';
+ if (messageDiv) messageDiv.style.display = 'none';
+
+ } else if (!hasServer) {
+ // State: No server configured
+ card.classList.add('needs-setup');
+ if (title) title.textContent = 'No Media Server';
+ if (subtitle) subtitle.textContent = 'Connect a server to get started';
+ if (scanBtn) scanBtn.style.display = 'none';
+ if (deepBtn) deepBtn.style.display = 'none';
+ if (statsRow) statsRow.style.display = 'none';
+ if (progressDiv) progressDiv.style.display = 'none';
+ if (messageDiv) {
+ messageDiv.style.display = '';
+ messageDiv.innerHTML = 'SoulSync needs a media server to manage your library. '
+ + 'Go to Settings '
+ + 'to connect Plex, Jellyfin, or Navidrome.';
+ }
+
+ } else if (!serverConnected) {
+ // State: Server configured but not connected
+ card.classList.add('needs-setup');
+ const serverName = _capitalize(serverType);
+ if (title) title.textContent = `${serverName} — Disconnected`;
+ if (subtitle) subtitle.textContent = 'Cannot reach your media server';
+ if (scanBtn) scanBtn.style.display = 'none';
+ if (deepBtn) deepBtn.style.display = 'none';
+ if (statsRow) statsRow.style.display = 'none';
+ if (progressDiv) progressDiv.style.display = 'none';
+ if (messageDiv) {
+ messageDiv.style.display = '';
+ messageDiv.innerHTML = `Your ${serverName} server is configured but not responding. `
+ + 'Check that it\'s running and the connection details are correct in '
+ + 'Settings.';
+ }
+
+ } else if (!hasData) {
+ // State: Server connected but library is empty
+ card.classList.add('empty-library');
+ const serverName = _capitalize(serverType);
+ if (title) title.textContent = `${serverName} Connected`;
+ if (subtitle) subtitle.textContent = 'Library database is empty';
+ if (scanBtn) {
+ scanBtn.style.display = '';
+ scanBtn.classList.remove('scanning');
+ scanLabel.textContent = 'Scan Now';
+ scanBtn.disabled = false;
+ }
+ if (deepBtn) deepBtn.style.display = 'none';
+ if (statsRow) statsRow.style.display = 'none';
+ if (progressDiv) progressDiv.style.display = 'none';
+ if (messageDiv) {
+ messageDiv.style.display = '';
+ messageDiv.innerHTML = 'Your server is connected but SoulSync hasn\'t imported your library yet. '
+ + 'Click Scan Now to pull your artists, albums, and tracks into SoulSync.';
+ }
+
+ } else {
+ // State: Healthy library with data
+ card.classList.add('has-data');
+ const serverName = _capitalize(serverType);
+ let lastRefreshText = 'Never';
+ if (lastUpdate) {
+ const d = new Date(lastUpdate);
+ if (!isNaN(d.getTime())) {
+ lastRefreshText = typeof _formatTimeAgo === 'function' ? _formatTimeAgo(d) : d.toLocaleDateString();
+ }
+ }
+ if (title) title.textContent = `${serverName} Library`;
+ if (subtitle) subtitle.textContent = `Last refreshed ${lastRefreshText}`;
+ if (scanBtn) {
+ scanBtn.style.display = '';
+ scanBtn.classList.remove('scanning');
+ scanLabel.textContent = 'Refresh';
+ scanBtn.disabled = false;
+ }
+ if (deepBtn) deepBtn.style.display = '';
+ if (statsRow) {
+ statsRow.style.display = '';
+ document.getElementById('library-status-artists').textContent = artists.toLocaleString();
+ document.getElementById('library-status-albums').textContent = albums.toLocaleString();
+ document.getElementById('library-status-tracks').textContent = tracks.toLocaleString();
+ document.getElementById('library-status-size').textContent = sizeMb < 1 ? `${Math.round(sizeMb * 1024)} KB` : `${sizeMb.toFixed(1)} MB`;
+ }
+ if (progressDiv) progressDiv.style.display = 'none';
+ if (messageDiv) messageDiv.style.display = 'none';
+ }
+}
+
+// Track last service status for library card
+let _lastServiceStatus = null;
+const _origFetchServiceStatus = typeof fetchAndUpdateServiceStatus === 'function' ? fetchAndUpdateServiceStatus : null;
+
+function _capitalize(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; }
+
+/**
+ * Dashboard library scan button handler — triggers incremental DB update.
+ */
+async function dashboardLibraryScan(fullRefresh = false) {
+ const scanBtn = document.getElementById('library-status-scan-btn');
+ const scanLabel = document.getElementById('library-status-scan-label');
+
+ // If already scanning, stop it
+ if (window._libraryStatusScanning) {
+ try {
+ await fetch('/api/database/update/stop', { method: 'POST' });
+ window._libraryStatusScanning = false;
+ showToast('Library scan stopped', 'info');
+ // Refresh the card
+ try {
+ const r = await fetch('/api/database/stats');
+ if (r.ok) updateLibraryStatusCard(await r.json());
+ } catch (e) {}
+ } catch (e) {
+ showToast('Failed to stop scan', 'error');
+ }
+ return;
+ }
+
+ // Start scan
+ try {
+ window._libraryStatusScanning = true;
+ updateLibraryStatusCard(null); // Update to scanning state
+
+ const response = await fetch('/api/database/update', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ full_refresh: fullRefresh })
+ });
+ const data = await response.json();
+ if (!data.success) {
+ window._libraryStatusScanning = false;
+ showToast(data.error || 'Failed to start scan', 'error');
+ return;
+ }
+
+ showToast('Library scan started', 'success');
+
+ // Poll for progress
+ const pollInterval = setInterval(async () => {
+ try {
+ const statusResp = await fetch('/api/database/update/status');
+ if (!statusResp.ok) return;
+ const status = await statusResp.json();
+
+ const phase = document.getElementById('library-status-phase');
+ const barFill = document.getElementById('library-status-bar-fill');
+ const detail = document.getElementById('library-status-progress-detail');
+
+ if (phase) phase.textContent = status.phase || 'Scanning...';
+ if (barFill) barFill.style.width = `${status.progress || 0}%`;
+ if (detail && status.processed !== undefined) {
+ detail.textContent = `${status.processed} / ${status.total || '?'}`;
+ }
+
+ if (status.status === 'completed' || status.status === 'error' || status.status === 'idle') {
+ clearInterval(pollInterval);
+ window._libraryStatusScanning = false;
+
+ if (status.status === 'completed') {
+ showToast('Library scan complete', 'success');
+ } else if (status.status === 'error') {
+ showToast(`Scan error: ${status.error_message || 'Unknown'}`, 'error');
+ }
+
+ // Refresh stats
+ try {
+ const r = await fetch('/api/database/stats');
+ if (r.ok) updateLibraryStatusCard(await r.json());
+ } catch (e) {}
+ }
+ } catch (e) {
+ clearInterval(pollInterval);
+ window._libraryStatusScanning = false;
+ }
+ }, 2000);
+
+ } catch (e) {
+ window._libraryStatusScanning = false;
+ showToast(`Scan failed: ${e.message}`, 'error');
+ }
+}
+
+/**
+ * Dashboard deep scan — finds new tracks, removes stale ones, preserves enrichment data.
+ */
+async function dashboardLibraryDeepScan() {
+ if (window._libraryStatusScanning) {
+ showToast('A scan is already running', 'warning');
+ return;
+ }
+
+ if (!await showConfirmDialog({
+ title: 'Deep Scan Library',
+ message: 'A deep scan re-checks every track in your media server library.\n\n' +
+ '• Adds any new tracks that were missed\n' +
+ '• Removes tracks no longer on your server\n' +
+ '• Preserves all existing metadata and enrichment data\n\n' +
+ 'This may take a while for large libraries. Continue?',
+ })) return;
+
+ // Use the same scan flow as dashboardLibraryScan but with deep_scan flag
+ try {
+ window._libraryStatusScanning = true;
+ updateLibraryStatusCard(null);
+
+ const response = await fetch('/api/database/update', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ deep_scan: true })
+ });
+ const data = await response.json();
+ if (!data.success) {
+ window._libraryStatusScanning = false;
+ showToast(data.error || 'Failed to start deep scan', 'error');
+ try { const r = await fetch('/api/database/stats'); if (r.ok) updateLibraryStatusCard(await r.json()); } catch (e) {}
+ return;
+ }
+
+ showToast('Deep scan started — this may take a while', 'success');
+
+ const pollInterval = setInterval(async () => {
+ try {
+ const statusResp = await fetch('/api/database/update/status');
+ if (!statusResp.ok) return;
+ const status = await statusResp.json();
+
+ const phase = document.getElementById('library-status-phase');
+ const barFill = document.getElementById('library-status-bar-fill');
+ const detail = document.getElementById('library-status-progress-detail');
+
+ if (phase) phase.textContent = status.phase || 'Deep scanning...';
+ if (barFill) barFill.style.width = `${status.progress || 0}%`;
+ if (detail && status.processed !== undefined) {
+ detail.textContent = `${status.processed} / ${status.total || '?'}`;
+ }
+
+ if (status.status === 'completed' || status.status === 'error' || status.status === 'idle') {
+ clearInterval(pollInterval);
+ window._libraryStatusScanning = false;
+
+ if (status.status === 'completed') {
+ showToast('Deep scan complete', 'success');
+ } else if (status.status === 'error') {
+ showToast(`Deep scan error: ${status.error_message || 'Unknown'}`, 'error');
+ }
+
+ try { const r = await fetch('/api/database/stats'); if (r.ok) updateLibraryStatusCard(await r.json()); } catch (e) {}
+ }
+ } catch (e) {
+ clearInterval(pollInterval);
+ window._libraryStatusScanning = false;
+ }
+ }, 2000);
+
+ } catch (e) {
+ window._libraryStatusScanning = false;
+ showToast(`Deep scan failed: ${e.message}`, 'error');
+ }
}
/**
@@ -38873,6 +39187,9 @@ async function fetchAndUpdateServiceStatus() {
const data = await response.json();
+ // Cache for library status card
+ _lastServiceStatus = data;
+
// Update service status indicators and text (dashboard)
updateServiceStatus('spotify', data.spotify);
updateServiceStatus('media-server', data.media_server);
diff --git a/webui/static/style.css b/webui/static/style.css
index d24f99f7..39ae1b17 100644
--- a/webui/static/style.css
+++ b/webui/static/style.css
@@ -55797,6 +55797,387 @@ body.reduce-effects *::after {
}
}
+/* ═══════════════════════════════════════════════════════════════════
+ DASHBOARD LIBRARY STATUS CARD
+ ═══════════════════════════════════════════════════════════════════ */
+
+.library-status-card {
+ background: linear-gradient(135deg,
+ rgba(20, 20, 20, 0.95) 0%,
+ rgba(14, 14, 14, 0.98) 100%);
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-top: 1px solid rgba(255, 255, 255, 0.14);
+ border-radius: 16px;
+ padding: 22px 24px;
+ box-shadow:
+ 0 6px 24px rgba(0, 0, 0, 0.35),
+ 0 2px 8px rgba(0, 0, 0, 0.2),
+ inset 0 1px 0 rgba(255, 255, 255, 0.08);
+ position: relative;
+ overflow: hidden;
+ transition: border-color 0.3s, box-shadow 0.3s;
+}
+
+.library-status-card:hover {
+ border-color: rgba(var(--accent-rgb), 0.15);
+ box-shadow:
+ 0 8px 28px rgba(0, 0, 0, 0.4),
+ 0 0 16px rgba(var(--accent-rgb), 0.04),
+ inset 0 1px 0 rgba(255, 255, 255, 0.1);
+}
+
+/* Accent top line */
+.library-status-card::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 3px;
+ background: linear-gradient(90deg,
+ transparent 0%,
+ rgba(var(--accent-rgb), 0.3) 20%,
+ rgba(var(--accent-rgb), 0.6) 50%,
+ rgba(var(--accent-rgb), 0.3) 80%,
+ transparent 100%);
+ opacity: 0;
+ transition: opacity 0.3s;
+}
+
+/* Animated background glow */
+.library-status-glow {
+ position: absolute;
+ top: -50%;
+ right: -20%;
+ width: 300px;
+ height: 300px;
+ border-radius: 50%;
+ background: radial-gradient(circle, rgba(var(--accent-rgb), 0.06) 0%, transparent 70%);
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity 0.5s;
+}
+
+.library-status-card.has-data .library-status-glow {
+ opacity: 1;
+}
+
+.library-status-card.has-data::before {
+ opacity: 1;
+}
+
+.library-status-card.needs-setup {
+ border-color: rgba(255, 193, 7, 0.15);
+}
+
+.library-status-card.needs-setup::before {
+ background: linear-gradient(90deg,
+ transparent, rgba(255, 193, 7, 0.4), rgba(255, 193, 7, 0.6), rgba(255, 193, 7, 0.4), transparent);
+ opacity: 1;
+}
+
+.library-status-card.needs-setup .library-status-glow {
+ background: radial-gradient(circle, rgba(255, 193, 7, 0.05) 0%, transparent 70%);
+ opacity: 1;
+}
+
+.library-status-card.empty-library::before {
+ background: linear-gradient(90deg,
+ transparent, rgba(59, 130, 246, 0.4), rgba(59, 130, 246, 0.6), rgba(59, 130, 246, 0.4), transparent);
+ opacity: 1;
+}
+
+.library-status-card.empty-library .library-status-glow {
+ background: radial-gradient(circle, rgba(59, 130, 246, 0.05) 0%, transparent 70%);
+ opacity: 1;
+}
+
+.library-status-card.scanning::before {
+ opacity: 1;
+ animation: scanPulse 2s ease-in-out infinite;
+}
+
+.library-status-card.scanning .library-status-glow {
+ opacity: 1;
+ animation: glowPulse 3s ease-in-out infinite;
+}
+
+@keyframes glowPulse {
+ 0%, 100% { opacity: 0.5; transform: scale(1); }
+ 50% { opacity: 1; transform: scale(1.1); }
+}
+
+.library-status-header {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+}
+
+.library-status-icon {
+ width: 44px;
+ height: 44px;
+ border-radius: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: linear-gradient(135deg, rgba(var(--accent-rgb), 0.15), rgba(var(--accent-rgb), 0.05));
+ color: rgb(var(--accent-rgb));
+ flex-shrink: 0;
+ border: 1px solid rgba(var(--accent-rgb), 0.1);
+}
+
+.library-status-card.needs-setup .library-status-icon {
+ background: linear-gradient(135deg, rgba(255, 193, 7, 0.15), rgba(255, 193, 7, 0.05));
+ color: #ffc107;
+ border-color: rgba(255, 193, 7, 0.1);
+}
+
+.library-status-card.empty-library .library-status-icon {
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.15), rgba(59, 130, 246, 0.05));
+ color: #60a5fa;
+ border-color: rgba(59, 130, 246, 0.1);
+}
+
+.library-status-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.library-status-title {
+ font-size: 15px;
+ font-weight: 700;
+ color: #fff;
+ margin: 0;
+ letter-spacing: -0.2px;
+}
+
+.library-status-subtitle {
+ font-size: 12px;
+ color: rgba(255, 255, 255, 0.4);
+ margin: 2px 0 0 0;
+ font-weight: 500;
+}
+
+.library-status-actions {
+ display: flex;
+ gap: 8px;
+ flex-shrink: 0;
+}
+
+.library-status-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 16px;
+ border-radius: 8px;
+ border: 1px solid rgba(var(--accent-rgb), 0.25);
+ background: rgba(var(--accent-rgb), 0.1);
+ color: rgb(var(--accent-light-rgb));
+ font-size: 12px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s;
+ white-space: nowrap;
+}
+
+.library-status-btn:hover {
+ background: rgba(var(--accent-rgb), 0.2);
+ border-color: rgba(var(--accent-rgb), 0.4);
+ transform: translateY(-1px);
+}
+
+.library-status-btn:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+ transform: none;
+}
+
+.library-status-btn-secondary {
+ background: rgba(255, 255, 255, 0.04);
+ border-color: rgba(255, 255, 255, 0.1);
+ color: rgba(255, 255, 255, 0.55);
+}
+
+.library-status-btn-secondary:hover {
+ background: rgba(255, 255, 255, 0.08);
+ border-color: rgba(255, 255, 255, 0.18);
+ color: rgba(255, 255, 255, 0.8);
+}
+
+.library-status-btn.scanning {
+ border-color: rgba(239, 68, 68, 0.3);
+ background: rgba(239, 68, 68, 0.1);
+ color: #f87171;
+}
+
+.library-status-stats {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 10px;
+ margin-top: 16px;
+ padding-top: 16px;
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
+}
+
+.library-status-stat {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 14px;
+ background: rgba(255, 255, 255, 0.025);
+ border: 1px solid rgba(255, 255, 255, 0.05);
+ border-radius: 10px;
+ transition: all 0.2s;
+}
+
+.library-status-stat:hover {
+ background: rgba(255, 255, 255, 0.04);
+ border-color: rgba(255, 255, 255, 0.08);
+}
+
+.library-status-stat-icon {
+ width: 32px;
+ height: 32px;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(var(--accent-rgb), 0.08);
+ color: rgba(var(--accent-rgb), 0.7);
+ flex-shrink: 0;
+}
+
+.library-status-stat-text {
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+ min-width: 0;
+}
+
+.library-status-stat-value {
+ font-size: 18px;
+ font-weight: 700;
+ color: #fff;
+ letter-spacing: -0.5px;
+ line-height: 1.1;
+ font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
+}
+
+.library-status-stat-label {
+ font-size: 10px;
+ font-weight: 500;
+ color: rgba(255, 255, 255, 0.3);
+ text-transform: uppercase;
+ letter-spacing: 0.8px;
+}
+
+.library-status-progress {
+ margin-top: 14px;
+ padding-top: 14px;
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
+}
+
+.library-status-phase {
+ font-size: 12px;
+ color: rgba(255, 255, 255, 0.5);
+ font-weight: 500;
+ margin-bottom: 8px;
+}
+
+.library-status-bar {
+ height: 5px;
+ background: rgba(255, 255, 255, 0.06);
+ border-radius: 3px;
+ overflow: hidden;
+}
+
+.library-status-bar-fill {
+ height: 100%;
+ background: linear-gradient(90deg, rgb(var(--accent-rgb)), rgb(var(--accent-light-rgb)));
+ border-radius: 3px;
+ transition: width 0.5s ease;
+ box-shadow: 0 0 12px rgba(var(--accent-rgb), 0.4);
+ position: relative;
+}
+
+.library-status-bar-fill::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.2) 50%, transparent 100%);
+ animation: barShimmer 2s ease-in-out infinite;
+}
+
+@keyframes barShimmer {
+ 0% { transform: translateX(-100%); }
+ 100% { transform: translateX(100%); }
+}
+
+.library-status-progress-detail {
+ font-size: 11px;
+ color: rgba(255, 255, 255, 0.35);
+ margin-top: 6px;
+ text-align: right;
+}
+
+.library-status-message {
+ margin-top: 12px;
+ padding: 12px 16px;
+ background: rgba(255, 255, 255, 0.025);
+ border: 1px solid rgba(255, 255, 255, 0.05);
+ border-radius: 8px;
+ font-size: 13px;
+ color: rgba(255, 255, 255, 0.55);
+ line-height: 1.5;
+}
+
+.library-status-message a,
+.library-status-message span.link {
+ color: rgb(var(--accent-light-rgb));
+ cursor: pointer;
+ text-decoration: none;
+ font-weight: 600;
+}
+
+.library-status-message a:hover,
+.library-status-message span.link:hover {
+ text-decoration: underline;
+}
+
+@media (max-width: 900px) {
+ .library-status-stats {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+@media (max-width: 768px) {
+ .library-status-header {
+ flex-wrap: wrap;
+ gap: 10px;
+ }
+
+ .library-status-actions {
+ width: 100%;
+ }
+
+ .library-status-btn {
+ flex: 1;
+ justify-content: center;
+ }
+
+ .library-status-stats {
+ grid-template-columns: 1fr 1fr;
+ gap: 8px;
+ }
+
+ .library-status-stat-value {
+ font-size: 15px;
+ }
+}
+
/* ═══════════════════════════════════════════════════════════════════
DASHBOARD TOOLS LINK CARD
═══════════════════════════════════════════════════════════════════ */