Dashboard bento grid redesign + responsive breakpoints
Replaces the old stacked dashboard with a bento grid: services, stats, library, syncs, tools, activity, enrichment each live in their own card. - 3-col on desktop (>=1500px), 2-col on laptop, 2-col tighter on tablet, 1-col stack on mobile (<700px). Sub-grids inside each card adapt at every breakpoint (service tiles 3-2-1, stat cards 3-2, gauge tiles 10-5-4-3-2). - Cards use the user's accent color for glow + hover border + CTA icons (was hardcoded per-card hues). - Mount fade-up with per-card stagger; subtle bloom drift; reduced-motion honored. - Enrichment row collapses the per-service gauge tile (hides the 3-stat row, scales the gauge SVG to fill the tile width) so all 10 services fit on one row at desktop. - Recent syncs stacks vertically inside its bento card instead of overflowing horizontally. - Every existing id, button, and JS hook preserved -- no behavior change, pure visual + responsive overhaul.
This commit is contained in:
parent
fc86535da4
commit
acce083675
3 changed files with 930 additions and 195 deletions
433
webui/index.html
433
webui/index.html
|
|
@ -619,222 +619,265 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-section">
|
||||
<h3 class="section-title">Service Status</h3>
|
||||
<div class="service-status-grid">
|
||||
<div class="service-card" id="metadata-source-service-card" data-status-ready="false">
|
||||
<div class="service-card-header">
|
||||
<span class="service-card-title" id="metadata-source-title">Metadata Source</span>
|
||||
<span class="service-card-indicator disconnected"
|
||||
id="metadata-source-status-indicator">●</span>
|
||||
</div>
|
||||
<p class="service-card-status-text" id="metadata-source-status-text">Disconnected</p>
|
||||
<p class="service-card-response-time" id="metadata-source-response-time">Response: --</p>
|
||||
<div class="service-card-footer">
|
||||
<button class="service-card-button"
|
||||
onclick="testDashboardConnection(getActiveMetadataSource())">Test Connection</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="service-card" id="media-server-service-card" data-status-ready="false">
|
||||
<div class="service-card-header">
|
||||
<span class="service-card-title" id="media-server-service-name">Media Server</span>
|
||||
<span class="service-card-indicator disconnected"
|
||||
id="media-server-status-indicator">●</span>
|
||||
</div>
|
||||
<p class="service-card-status-text" id="media-server-status-text">Disconnected</p>
|
||||
<p class="service-card-response-time" id="media-server-response-time">Response: --</p>
|
||||
<div class="service-card-footer">
|
||||
<button class="service-card-button" onclick="testDashboardConnection('server')">Test
|
||||
Connection</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="service-card" id="soulseek-service-card" data-status-ready="false">
|
||||
<div class="service-card-header">
|
||||
<span class="service-card-title" id="download-source-title">Download Source</span>
|
||||
<span class="service-card-indicator disconnected"
|
||||
id="soulseek-status-indicator">●</span>
|
||||
</div>
|
||||
<p class="service-card-status-text" id="soulseek-status-text">Disconnected</p>
|
||||
<p class="service-card-response-time" id="soulseek-response-time">Response: --</p>
|
||||
<div class="service-card-footer">
|
||||
<button class="service-card-button"
|
||||
onclick="testDashboardConnection('soulseek')">Test Connection</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="enrichment-section" id="enrichment-pills-section" style="display:none">
|
||||
<div class="enrichment-section-header">
|
||||
<span class="enrichment-section-label">Enrichment Services</span>
|
||||
</div>
|
||||
<div class="enrichment-status-grid" id="enrichment-status-grid">
|
||||
<!-- Legacy pills — hidden when rate monitor active -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ═══════════════════════════════════════════════════════════════
|
||||
Bento dashboard — bento-style mixed-size cards. Each card has a
|
||||
bold title + short subtitle + body region + optional actions.
|
||||
Layout flows in a 3-column grid; cards span 1/2/3 columns to
|
||||
create visual hierarchy. All existing IDs + child classes
|
||||
preserved so JS that updates the dashboard keeps working.
|
||||
═══════════════════════════════════════════════════════════════ -->
|
||||
<div class="dash-grid">
|
||||
|
||||
<!-- API Rate Monitor Section (replaces enrichment pills with richer cards) -->
|
||||
<div class="dashboard-section" id="rate-monitor-section">
|
||||
<h3 class="section-title">Enrichment Services</h3>
|
||||
<div class="rate-monitor-grid" id="rate-monitor-grid">
|
||||
<!-- Populated dynamically by JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Library Status Card -->
|
||||
<div class="dashboard-section">
|
||||
<div class="library-status-card" id="library-status-card">
|
||||
<!-- Animated background accent -->
|
||||
<div class="library-status-glow"></div>
|
||||
|
||||
<div class="library-status-header">
|
||||
<div class="library-status-icon" id="library-status-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/><line x1="9" y1="7" x2="16" y2="7"/><line x1="9" y1="11" x2="14" y2="11"/></svg>
|
||||
</div>
|
||||
<div class="library-status-info">
|
||||
<h4 class="library-status-title" id="library-status-title">Library</h4>
|
||||
<p class="library-status-subtitle" id="library-status-subtitle">Checking status...</p>
|
||||
</div>
|
||||
<div class="library-status-actions" id="library-status-actions">
|
||||
<button class="library-status-btn" id="library-status-scan-btn" style="display: none;" onclick="dashboardLibraryScan(false)">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
||||
<span id="library-status-scan-label">Refresh</span>
|
||||
</button>
|
||||
<button class="library-status-btn library-status-btn-secondary" id="library-status-deep-btn" style="display: none;" onclick="dashboardLibraryDeepScan()">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
|
||||
Deep Scan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="library-status-stats" id="library-status-stats" style="display: none;">
|
||||
<div class="library-status-stat">
|
||||
<div class="library-status-stat-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
<!-- Card: Service Status (wide, top-left) -->
|
||||
<article class="dash-card" data-card="services">
|
||||
<header class="dash-card__head">
|
||||
<h3 class="dash-card__title">Service Status</h3>
|
||||
<p class="dash-card__sub">Connection health for every service SoulSync uses.</p>
|
||||
</header>
|
||||
<div class="dash-card__body">
|
||||
<div class="service-status-grid">
|
||||
<div class="service-card" id="metadata-source-service-card" data-status-ready="false">
|
||||
<div class="service-card-header">
|
||||
<span class="service-card-title" id="metadata-source-title">Metadata Source</span>
|
||||
<span class="service-card-indicator disconnected" id="metadata-source-status-indicator">●</span>
|
||||
</div>
|
||||
<p class="service-card-status-text" id="metadata-source-status-text">Disconnected</p>
|
||||
<p class="service-card-response-time" id="metadata-source-response-time">Response: --</p>
|
||||
<div class="service-card-footer">
|
||||
<button class="service-card-button" onclick="testDashboardConnection(getActiveMetadataSource())">Test</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="library-status-stat-text">
|
||||
<span class="library-status-stat-value" id="library-status-artists">0</span>
|
||||
<span class="library-status-stat-label">Artists</span>
|
||||
<div class="service-card" id="media-server-service-card" data-status-ready="false">
|
||||
<div class="service-card-header">
|
||||
<span class="service-card-title" id="media-server-service-name">Media Server</span>
|
||||
<span class="service-card-indicator disconnected" id="media-server-status-indicator">●</span>
|
||||
</div>
|
||||
<p class="service-card-status-text" id="media-server-status-text">Disconnected</p>
|
||||
<p class="service-card-response-time" id="media-server-response-time">Response: --</p>
|
||||
<div class="service-card-footer">
|
||||
<button class="service-card-button" onclick="testDashboardConnection('server')">Test</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="service-card" id="soulseek-service-card" data-status-ready="false">
|
||||
<div class="service-card-header">
|
||||
<span class="service-card-title" id="download-source-title">Download Source</span>
|
||||
<span class="service-card-indicator disconnected" id="soulseek-status-indicator">●</span>
|
||||
</div>
|
||||
<p class="service-card-status-text" id="soulseek-status-text">Disconnected</p>
|
||||
<p class="service-card-response-time" id="soulseek-response-time">Response: --</p>
|
||||
<div class="service-card-footer">
|
||||
<button class="service-card-button" onclick="testDashboardConnection('soulseek')">Test</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="library-status-stat">
|
||||
<div class="library-status-stat-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
||||
<div class="enrichment-section" id="enrichment-pills-section" style="display:none">
|
||||
<div class="enrichment-section-header">
|
||||
<span class="enrichment-section-label">Enrichment Services</span>
|
||||
</div>
|
||||
<div class="library-status-stat-text">
|
||||
<span class="library-status-stat-value" id="library-status-albums">0</span>
|
||||
<span class="library-status-stat-label">Albums</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="library-status-stat">
|
||||
<div class="library-status-stat-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
|
||||
</div>
|
||||
<div class="library-status-stat-text">
|
||||
<span class="library-status-stat-value" id="library-status-tracks">0</span>
|
||||
<span class="library-status-stat-label">Tracks</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="library-status-stat">
|
||||
<div class="library-status-stat-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>
|
||||
</div>
|
||||
<div class="library-status-stat-text">
|
||||
<span class="library-status-stat-value" id="library-status-size">--</span>
|
||||
<span class="library-status-stat-label">DB Size</span>
|
||||
<div class="enrichment-status-grid" id="enrichment-status-grid">
|
||||
<!-- Legacy pills — hidden when rate monitor active -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div class="library-status-progress" id="library-status-progress" style="display: none;">
|
||||
<div class="library-status-phase" id="library-status-phase">Scanning...</div>
|
||||
<div class="library-status-bar">
|
||||
<div class="library-status-bar-fill" id="library-status-bar-fill" style="width: 0%;"></div>
|
||||
<!-- Card: System Stats (row 1, between Services and Library) -->
|
||||
<article class="dash-card" data-card="stats">
|
||||
<header class="dash-card__head">
|
||||
<h3 class="dash-card__title">System Stats</h3>
|
||||
<p class="dash-card__sub">Live performance metrics.</p>
|
||||
</header>
|
||||
<div class="dash-card__body">
|
||||
<div class="stats-grid-dashboard">
|
||||
<div class="stat-card-dashboard" id="active-downloads-card">
|
||||
<p class="stat-card-title">Active Downloads</p>
|
||||
<p class="stat-card-value">0</p>
|
||||
<p class="stat-card-subtitle">Currently downloading</p>
|
||||
</div>
|
||||
<div class="stat-card-dashboard" id="finished-downloads-card">
|
||||
<p class="stat-card-title">Finished Downloads</p>
|
||||
<p class="stat-card-value">0</p>
|
||||
<p class="stat-card-subtitle">Completed this session</p>
|
||||
</div>
|
||||
<div class="stat-card-dashboard" id="download-speed-card">
|
||||
<p class="stat-card-title">Download Speed</p>
|
||||
<p class="stat-card-value">0 KB/s</p>
|
||||
<p class="stat-card-subtitle">Combined speed</p>
|
||||
</div>
|
||||
<div class="stat-card-dashboard" id="active-syncs-card">
|
||||
<p class="stat-card-title">Active Syncs</p>
|
||||
<p class="stat-card-value">0</p>
|
||||
<p class="stat-card-subtitle">Playlists syncing</p>
|
||||
</div>
|
||||
<div class="stat-card-dashboard" id="uptime-card">
|
||||
<p class="stat-card-title">System Uptime</p>
|
||||
<p class="stat-card-value">0m</p>
|
||||
<p class="stat-card-subtitle">Application runtime</p>
|
||||
</div>
|
||||
<div class="stat-card-dashboard" id="memory-card">
|
||||
<p class="stat-card-title">Memory Usage</p>
|
||||
<p class="stat-card-value">--</p>
|
||||
<p class="stat-card-subtitle">Current usage</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="library-status-progress-detail" id="library-status-progress-detail">0 / 0</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div class="library-status-message" id="library-status-message" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Card: Library (row 1) -->
|
||||
<article class="dash-card" data-card="library">
|
||||
<header class="dash-card__head">
|
||||
<h3 class="dash-card__title">Library</h3>
|
||||
<p class="dash-card__sub">Your collection at a glance.</p>
|
||||
</header>
|
||||
<div class="dash-card__body">
|
||||
<div class="library-status-card" id="library-status-card">
|
||||
<div class="library-status-glow"></div>
|
||||
<div class="library-status-header">
|
||||
<div class="library-status-icon" id="library-status-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/><line x1="9" y1="7" x2="16" y2="7"/><line x1="9" y1="11" x2="14" y2="11"/></svg>
|
||||
</div>
|
||||
<div class="library-status-info">
|
||||
<h4 class="library-status-title" id="library-status-title">Library</h4>
|
||||
<p class="library-status-subtitle" id="library-status-subtitle">Checking status...</p>
|
||||
</div>
|
||||
<div class="library-status-actions" id="library-status-actions">
|
||||
<button class="library-status-btn" id="library-status-scan-btn" style="display: none;" onclick="dashboardLibraryScan(false)">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
||||
<span id="library-status-scan-label">Refresh</span>
|
||||
</button>
|
||||
<button class="library-status-btn library-status-btn-secondary" id="library-status-deep-btn" style="display: none;" onclick="dashboardLibraryDeepScan()">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
|
||||
Deep Scan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="library-status-stats" id="library-status-stats" style="display: none;">
|
||||
<div class="library-status-stat">
|
||||
<div class="library-status-stat-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
</div>
|
||||
<div class="library-status-stat-text">
|
||||
<span class="library-status-stat-value" id="library-status-artists">0</span>
|
||||
<span class="library-status-stat-label">Artists</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="library-status-stat">
|
||||
<div class="library-status-stat-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
||||
</div>
|
||||
<div class="library-status-stat-text">
|
||||
<span class="library-status-stat-value" id="library-status-albums">0</span>
|
||||
<span class="library-status-stat-label">Albums</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="library-status-stat">
|
||||
<div class="library-status-stat-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
|
||||
</div>
|
||||
<div class="library-status-stat-text">
|
||||
<span class="library-status-stat-value" id="library-status-tracks">0</span>
|
||||
<span class="library-status-stat-label">Tracks</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="library-status-stat">
|
||||
<div class="library-status-stat-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>
|
||||
</div>
|
||||
<div class="library-status-stat-text">
|
||||
<span class="library-status-stat-value" id="library-status-size">--</span>
|
||||
<span class="library-status-stat-label">DB Size</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="library-status-progress" id="library-status-progress" style="display: none;">
|
||||
<div class="library-status-phase" id="library-status-phase">Scanning...</div>
|
||||
<div class="library-status-bar">
|
||||
<div class="library-status-bar-fill" id="library-status-bar-fill" style="width: 0%;"></div>
|
||||
</div>
|
||||
<div class="library-status-progress-detail" id="library-status-progress-detail">0 / 0</div>
|
||||
</div>
|
||||
<div class="library-status-message" id="library-status-message" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- Recent Syncs Section -->
|
||||
<div class="dashboard-section">
|
||||
<h3 class="section-title">Recent Syncs</h3>
|
||||
<div class="sync-history-cards" id="sync-history-cards">
|
||||
<!-- Dynamically populated by JS -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- Card: Recent Syncs (row 2) -->
|
||||
<article class="dash-card" data-card="syncs">
|
||||
<header class="dash-card__head">
|
||||
<h3 class="dash-card__title">Recent Syncs</h3>
|
||||
<p class="dash-card__sub">Playlists you've synced.</p>
|
||||
</header>
|
||||
<div class="dash-card__body">
|
||||
<div class="sync-history-cards" id="sync-history-cards">
|
||||
<!-- Dynamically populated by JS -->
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div class="dashboard-section">
|
||||
<h3 class="section-title">System Statistics</h3>
|
||||
<div class="stats-grid-dashboard">
|
||||
<div class="stat-card-dashboard" id="active-downloads-card">
|
||||
<p class="stat-card-title">Active Downloads</p>
|
||||
<p class="stat-card-value">0</p>
|
||||
<p class="stat-card-subtitle">Currently downloading</p>
|
||||
<!-- Card: Tools (compact, top-right) — entry point to maintenance ops -->
|
||||
<article class="dash-card dash-card--cta" data-card="tools" onclick="navigateToPage('tools')">
|
||||
<header class="dash-card__head">
|
||||
<h3 class="dash-card__title">Tools</h3>
|
||||
<p class="dash-card__sub">Database, scanning, backups, cache, maintenance & more.</p>
|
||||
</header>
|
||||
<div class="dash-card__body dash-card__body--cta">
|
||||
<div class="dash-card__cta-icon">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>
|
||||
</div>
|
||||
<div class="dash-card__cta-label">Open Tools</div>
|
||||
<svg class="dash-card__cta-arrow" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
|
||||
</div>
|
||||
<div class="stat-card-dashboard" id="finished-downloads-card">
|
||||
<p class="stat-card-title">Finished Downloads</p>
|
||||
<p class="stat-card-value">0</p>
|
||||
<p class="stat-card-subtitle">Completed this session</p>
|
||||
</div>
|
||||
<div class="stat-card-dashboard" id="download-speed-card">
|
||||
<p class="stat-card-title">Download Speed</p>
|
||||
<p class="stat-card-value">0 KB/s</p>
|
||||
<p class="stat-card-subtitle">Combined speed</p>
|
||||
</div>
|
||||
<div class="stat-card-dashboard" id="active-syncs-card">
|
||||
<p class="stat-card-title">Active Syncs</p>
|
||||
<p class="stat-card-value">0</p>
|
||||
<p class="stat-card-subtitle">Playlists syncing</p>
|
||||
</div>
|
||||
<div class="stat-card-dashboard" id="uptime-card">
|
||||
<p class="stat-card-title">System Uptime</p>
|
||||
<p class="stat-card-value">0m</p>
|
||||
<p class="stat-card-subtitle">Application runtime</p>
|
||||
</div>
|
||||
<div class="stat-card-dashboard" id="memory-card">
|
||||
<p class="stat-card-title">Memory Usage</p>
|
||||
<p class="stat-card-value">--</p>
|
||||
<p class="stat-card-subtitle">Current usage</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div class="dashboard-section" id="dashboard-active-downloads-section" style="display: none;">
|
||||
<h3 class="section-title">Active Downloads</h3>
|
||||
<div id="dashboard-downloads-container"></div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-section">
|
||||
<h3 class="section-title">Tools & Operations</h3>
|
||||
<div class="dashboard-tools-link" onclick="navigateToPage('tools')">
|
||||
<div class="dashboard-tools-link-content">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.5)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>
|
||||
<!-- Card: Recent Activity (row 2, after Tools) -->
|
||||
<article class="dash-card" data-card="activity">
|
||||
<header class="dash-card__head dash-card__head--withaction">
|
||||
<div>
|
||||
<span class="dashboard-tools-link-title">Database, scanning, backups, cache, maintenance & more</span>
|
||||
<h3 class="dash-card__title">Recent Activity</h3>
|
||||
<p class="dash-card__sub">What just happened.</p>
|
||||
</div>
|
||||
<button class="dash-card__head-btn" onclick="openLibraryHistoryModal()" title="View full library history">Download History</button>
|
||||
</header>
|
||||
<div class="dash-card__body">
|
||||
<div class="activity-feed-container" id="dashboard-activity-feed">
|
||||
<div class="activity-item">
|
||||
<span class="activity-icon">📊</span>
|
||||
<div class="activity-text-content">
|
||||
<p class="activity-title">System Started</p>
|
||||
<p class="activity-subtitle">Dashboard initialized successfully</p>
|
||||
</div>
|
||||
<p class="activity-time">Now</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.3)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
|
||||
<div class="dashboard-section">
|
||||
<div class="section-title-row">
|
||||
<h3 class="section-title">Recent Activity</h3>
|
||||
<button class="library-history-btn" onclick="openLibraryHistoryModal()" title="View full library history">Download History</button>
|
||||
</div>
|
||||
<div class="activity-feed-container" id="dashboard-activity-feed">
|
||||
<div class="activity-item">
|
||||
<span class="activity-icon">📊</span>
|
||||
<div class="activity-text-content">
|
||||
<p class="activity-title">System Started</p>
|
||||
<p class="activity-subtitle">Dashboard initialized successfully</p>
|
||||
</div>
|
||||
<p class="activity-time">Now</p>
|
||||
<!-- Card: Active Downloads (full-width, shows when downloads in flight) -->
|
||||
<article class="dash-card dash-card--full" id="dashboard-active-downloads-section" style="display: none;" data-card="active-downloads">
|
||||
<header class="dash-card__head">
|
||||
<h3 class="dash-card__title">Active Downloads</h3>
|
||||
<p class="dash-card__sub">In-flight transfers from your sources.</p>
|
||||
</header>
|
||||
<div class="dash-card__body">
|
||||
<div id="dashboard-downloads-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- Card: Enrichment Detail (compact, paired with System Stats on the left) -->
|
||||
<article class="dash-card dash-card--full" id="rate-monitor-section" data-card="enrichment">
|
||||
<header class="dash-card__head">
|
||||
<h3 class="dash-card__title">Enrichment Services</h3>
|
||||
<p class="dash-card__sub">API rate monitoring across providers.</p>
|
||||
</header>
|
||||
<div class="dash-card__body">
|
||||
<div class="rate-monitor-grid" id="rate-monitor-grid">
|
||||
<!-- Populated dynamically by JS -->
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
|
||||
<!-- Card: Recent Syncs (full-width) -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3416,6 +3416,7 @@ const WHATS_NEW = {
|
|||
'2.5.2': [
|
||||
// --- May 13, 2026 — 2.5.2 release ---
|
||||
{ date: 'May 13, 2026 — 2.5.2 release' },
|
||||
{ title: 'Dashboard Bento Redesign', desc: 'rebuilt the dashboard as a bento grid. every section now lives in its own card with an accent-tinted glow that follows your theme. cards fade up on first paint with a staggered reveal. layout adapts: 3-col on desktop (≥1500px), 2-col on laptop, 2-col tighter on tablet, single-column on mobile (<700px). enrichment service gauges ride a single 10-tile row at desktop and wrap to 5 / 4 / 3 / 2 as space tightens. system stats render 3-up across 2 rows so all 6 metrics fit without scrolling. recent syncs stack vertically inside their card. service status, library, tools, recent activity all slot into the grid. every existing button + id preserved — pure visual + responsive overhaul.', page: 'home' },
|
||||
{ title: 'Retag No Longer Strips LYRICS Tag Without Rewriting', desc: 'discord report (netti93): retag tool was clearing the LYRICS / USLT tag and never rewriting it, while the download flow correctly embeds lyrics. asymmetry trace: download pipeline (`core/imports/pipeline.py`) calls `enhance_file_metadata` (clears all tags) then `generate_lrc_file` (writes .lrc sidecar + embeds USLT). retag (`core/library/retag.py`) only called the first half — `enhance_file_metadata` cleared USLT and there was no follow-up to restore it. fix 1: retag now calls `generate_lrc_file` after `enhance_file_metadata`, mirroring the download flow. injectable via `RetagDeps.generate_lrc_file` (optional default for backward compat). fix 2: `lyrics_client.create_lrc_file` used to short-circuit when an .lrc/.txt sidecar already existed (the typical retag case — sidecar moved alongside the audio). pre-fix: returned True without re-embedding USLT. post-fix: reads the existing sidecar and re-embeds the USLT tag. download flow unaffected (no sidecar at fetch time → original LRClib path runs). 7 boundary tests pin: existing .lrc triggers re-embed, existing .txt triggers re-embed, empty sidecar skips embed, unreadable sidecar swallows error, no sidecar falls through to LRClib (download path), `RetagDeps.generate_lrc_file` field accepted + optional for backward compat.', page: 'tools' },
|
||||
{ title: 'Track Number Tag No Longer Writes "6/0" When Album Total Is Unknown', desc: 'discord report (netti93): downloaded album tracks were tagged with `TRCK = "6/0"` instead of `"6/13"` when source data lacked total_tracks. retag tool wrote correct `"6/13"` because `core/tag_writer.py` already handled the case. trace: `core/metadata/enrichment.py:105` formatted unconditionally as `f"{track_number}/{total_tracks}"` and many album-dict construction sites pass `total_tracks: 0` (per `types.py`, 0 means "unknown" — not a real count). that 0 propagated straight to disk. fix at the consumer boundary so every album-dict constructor stays unchanged: lifted to pure helper `core/metadata/track_number_format.py:format_track_number_tag` that drops the `/N` suffix when total is 0 / None / negative — emits just `"6"` instead. matches retag\'s behavior + ID3 spec convention (TRCK can be `"N"` or `"N/M"`). MP4 trkn tuple gets the same treatment via `format_track_number_tuple` returning `(6, 0)` per spec\'s "unknown total" marker. 16 boundary tests pin every shape: known total / zero total / none total / none track / zero track / negative inputs / string coercion / unparseable strings / floats truncate.', page: 'tools' },
|
||||
{ title: 'AcoustID Scanner: Multi-Candidate Match + Duration Guard + Multi-Value Retag', desc: 'discord report (foxxify) issue #587: scanner produced false-positive "Wrong Song" findings for tracks where AcoustID returned multiple recordings per fingerprint and the top match was a wrong-credited recording (different MB entry sharing the same fingerprint). also: applying an AcoustID match retag stripped multi-value ARTISTS tags. three coordinated fixes per codex diagnosis. fix 1: scanner now iterates ALL AcoustID candidates (not just `recordings[0]`) — if any candidate matches expected title + artist, no finding. lifted to a shared pure helper `core/matching/acoustid_candidates.py:find_matching_recording` so both verifier and scanner use the same logic. fix 2: duration guard catches fingerprint hash collisions (foxxify\'s 17-minute mashup edit fingerprinted to a 5-minute late-70s japanese hiphop track — different songs, same fingerprint hash collision on a sampled section). when file duration vs AcoustID candidate duration differs by more than max(60s, 35%), the scanner skips the finding. fix 3: scanner retag (`_fix_wrong_song`) was bypassing the user\'s `metadata_enhancement.tags.write_multi_artist` setting because `write_tags_to_file` only wrote single-string TPE1. now extended with optional `artists_list` parameter that, when supplied + setting on, writes the multi-value tag (TXXX:Artists for ID3, `artists` key for vorbis/opus/flac, list-form `\xa9ART` for mp4) — exact same behavior as the post-download enrichment pipeline. AcoustID retag derives the per-artist list by splitting AcoustID\'s credit on the same separators (comma / ampersand / feat. / ft. / etc) the matching layer already uses. AcoustID version-mismatch gate left intact (still correctly catches genuinely-wrong files). 15 tests on the candidate helper + duration guard, 13 tests on the multi-value tag write path, 4 new scanner regression tests pinning every shape: lower-ranked candidate match suppression, no-suppression when no candidate matches, duration mismatch skip, no-skip when duration matches.', page: 'tools' },
|
||||
|
|
@ -3837,6 +3838,20 @@ const WHATS_NEW = {
|
|||
// Section shape: { title, description, features: [bullet strings],
|
||||
// usage_note?: 'optional hint shown at the bottom' }
|
||||
const VERSION_MODAL_SECTIONS = [
|
||||
{
|
||||
title: "Dashboard Got A Bento Redesign",
|
||||
description: "old dashboard had a lot of wasted space and sections fighting for attention. rebuilt as a bento grid with accent-tinted cards and subtle motion.",
|
||||
features: [
|
||||
"• every section lives in its own card with a soft accent-tinted glow matching your theme color",
|
||||
"• cards fade up on first paint with a staggered reveal — feels alive without being noisy",
|
||||
"• layout adapts: 3-col bento on desktop (≥1500px), 2-col on laptop, 2-col tighter on tablet, single-column on mobile",
|
||||
"• enrichment service gauges ride a single 10-tile row at desktop and wrap to 5 / 4 / 3 / 2 as space tightens",
|
||||
"• system stats render 3-up over 2 rows so all 6 metrics fit without scrolling",
|
||||
"• recent syncs stack vertically inside their card so you can scan them at a glance",
|
||||
"• every existing button + status indicator + id preserved — same dashboard, just laid out properly",
|
||||
],
|
||||
usage_note: "nothing to configure — visit the home page to see it",
|
||||
},
|
||||
{
|
||||
title: "Big Sync Sessions No Longer Wedge After 2-3 Hours",
|
||||
description: "github issue #499 (bafoed): downloading a big initial sync from spotify playlists worked for 2-3 hours then silently stopped. 3 active tasks stuck in \"searching\" state, replaced every ~10 min, slskd ui showed no actual activity. only fix was restarting the container.",
|
||||
|
|
|
|||
|
|
@ -60109,3 +60109,680 @@ body[data-artist-source="source"] #artist-detail-page #library-artist-enhance-bt
|
|||
.hifi-instance-remove:hover {
|
||||
color: #ef5350;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
BENTO DASHBOARD — polished mixed-size card grid for the dashboard page.
|
||||
|
||||
Each card has bold title + dim subtitle + hero body region + optional
|
||||
actions. Cards span 1, 2, or 3 of the 3 grid columns to create
|
||||
visual hierarchy. Preserves every JS-relied class hook + ID by
|
||||
wrapping existing markup rather than replacing it.
|
||||
|
||||
Inspired by the anthropic.com "Do more with Claude" tile layout —
|
||||
subtle border, soft shadow, hover lift, generous breathing room,
|
||||
per-card accent hue tints.
|
||||
========================================================================== */
|
||||
|
||||
.dash-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 18px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.dash-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 22px 24px;
|
||||
background: linear-gradient(165deg,
|
||||
rgba(34, 34, 38, 0.62) 0%,
|
||||
rgba(22, 22, 26, 0.74) 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.13);
|
||||
border-radius: 18px;
|
||||
box-shadow:
|
||||
0 6px 24px rgba(0, 0, 0, 0.28),
|
||||
0 2px 8px rgba(0, 0, 0, 0.18),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
transition:
|
||||
border-color 0.24s ease,
|
||||
transform 0.18s ease,
|
||||
box-shadow 0.24s ease;
|
||||
overflow: hidden;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
/* Subtle accent bloom in the top-left corner — adds depth without
|
||||
competing with content. Per-card hue via the data-card selector
|
||||
below. */
|
||||
.dash-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
rgba(var(--accent-rgb), 0.08) 0%,
|
||||
transparent 60%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
.dash-card:hover {
|
||||
border-color: rgba(var(--accent-rgb), 0.35);
|
||||
transform: translateY(-3px);
|
||||
box-shadow:
|
||||
0 12px 40px rgba(0, 0, 0, 0.40),
|
||||
0 4px 14px rgba(0, 0, 0, 0.22),
|
||||
0 0 28px rgba(var(--accent-rgb), 0.10),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.10);
|
||||
}
|
||||
|
||||
.dash-card:hover::before {
|
||||
opacity: 1.6;
|
||||
filter: blur(2px) saturate(1.2);
|
||||
}
|
||||
|
||||
/* Per-card accent bloom — all driven by user accent, only the position
|
||||
and intensity vary so each card still feels distinct. */
|
||||
.dash-card::before {
|
||||
transition: opacity 0.4s ease, filter 0.4s ease;
|
||||
animation: dashBloomDrift 12s ease-in-out infinite;
|
||||
}
|
||||
.dash-card[data-card="services"]::before { background: radial-gradient(ellipse at 20% 0%, rgba(var(--accent-rgb), 0.16) 0%, transparent 60%); }
|
||||
.dash-card[data-card="library"]::before { background: radial-gradient(ellipse at 70% 10%, rgba(var(--accent-rgb), 0.14) 0%, transparent 60%); animation-delay: -2s; }
|
||||
.dash-card[data-card="stats"]::before { background: radial-gradient(ellipse at 50% 0%, rgba(var(--accent-rgb), 0.13) 0%, transparent 60%); animation-delay: -4s; }
|
||||
.dash-card[data-card="activity"]::before { background: radial-gradient(ellipse at 30% 20%, rgba(var(--accent-rgb), 0.14) 0%, transparent 60%); animation-delay: -6s; }
|
||||
.dash-card[data-card="syncs"]::before { background: radial-gradient(ellipse at 80% 0%, rgba(var(--accent-rgb), 0.13) 0%, transparent 60%); animation-delay: -8s; }
|
||||
.dash-card[data-card="enrichment"]::before { background: radial-gradient(ellipse at 50% 0%, rgba(var(--accent-rgb), 0.12) 0%, transparent 70%); animation-delay: -3s; }
|
||||
.dash-card[data-card="tools"]::before { background: radial-gradient(ellipse at 30% 10%, rgba(var(--accent-rgb), 0.18) 0%, transparent 60%); animation-delay: -5s; }
|
||||
.dash-card[data-card="active-downloads"]::before { background: radial-gradient(ellipse at 50% 0%, rgba(var(--accent-rgb), 0.16) 0%, transparent 60%); animation-delay: -7s; }
|
||||
|
||||
/* Subtle bloom drift — keeps cards alive without being noisy */
|
||||
@keyframes dashBloomDrift {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); opacity: 1; }
|
||||
50% { transform: translate(6%, 4%) scale(1.08); opacity: 1.25; }
|
||||
}
|
||||
|
||||
/* Mount stagger — cards fade up on first paint (and on page revisit) */
|
||||
@keyframes dashCardMount {
|
||||
from { opacity: 0; transform: translateY(14px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.dash-card {
|
||||
animation: dashCardMount 0.55s cubic-bezier(0.22, 0.61, 0.36, 1) both;
|
||||
}
|
||||
.dash-card[data-card="services"] { animation-delay: 0.00s; }
|
||||
.dash-card[data-card="stats"] { animation-delay: 0.06s; }
|
||||
.dash-card[data-card="library"] { animation-delay: 0.12s; }
|
||||
.dash-card[data-card="syncs"] { animation-delay: 0.18s; }
|
||||
.dash-card[data-card="tools"] { animation-delay: 0.24s; }
|
||||
.dash-card[data-card="activity"] { animation-delay: 0.30s; }
|
||||
.dash-card[data-card="enrichment"] { animation-delay: 0.36s; }
|
||||
.dash-card[data-card="active-downloads"] { animation-delay: 0.20s; }
|
||||
|
||||
/* Honor reduced-motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.dash-card { animation: none; }
|
||||
.dash-card::before { animation: none; }
|
||||
}
|
||||
|
||||
/* Span modifiers */
|
||||
.dash-card--wide { grid-column: span 2; }
|
||||
.dash-card--full { grid-column: 1 / -1; }
|
||||
|
||||
/* Card head */
|
||||
.dash-card__head {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.dash-card__head--withaction {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
.dash-card__title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
letter-spacing: -0.015em;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.dash-card__sub {
|
||||
font-size: 12.5px;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
margin: 4px 0 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.dash-card__head-btn {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 8px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 7px 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
white-space: nowrap;
|
||||
font-family: inherit;
|
||||
}
|
||||
.dash-card__head-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.16);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Card body */
|
||||
.dash-card__body {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* CTA card variant (Tools) — entire card is clickable */
|
||||
.dash-card--cta {
|
||||
cursor: pointer;
|
||||
}
|
||||
.dash-card--cta:hover .dash-card__cta-arrow {
|
||||
transform: translateX(4px);
|
||||
color: rgb(var(--accent-rgb));
|
||||
}
|
||||
.dash-card--cta:hover .dash-card__cta-icon {
|
||||
background: rgba(var(--accent-rgb), 0.22);
|
||||
transform: scale(1.06) rotate(-2deg);
|
||||
box-shadow: 0 0 18px rgba(var(--accent-rgb), 0.35);
|
||||
}
|
||||
.dash-card__body--cta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
.dash-card__cta-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
background: rgba(var(--accent-rgb), 0.14);
|
||||
border: 1px solid rgba(var(--accent-rgb), 0.26);
|
||||
color: rgb(var(--accent-rgb));
|
||||
transition: background 0.24s ease, transform 0.24s ease, box-shadow 0.24s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dash-card__cta-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
.dash-card__cta-arrow {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
transition: transform 0.2s ease, color 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Body overrides for embedded sub-grids — make them compact for bento */
|
||||
|
||||
/* Service status: 3 service cards side-by-side in a tighter grid */
|
||||
.dash-card[data-card="services"] .service-status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.dash-card[data-card="services"] .service-card {
|
||||
background: rgba(255, 255, 255, 0.025);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 12px;
|
||||
padding: 14px 14px;
|
||||
min-height: 0;
|
||||
box-shadow: none;
|
||||
transition: background 0.18s ease, border-color 0.18s ease;
|
||||
}
|
||||
.dash-card[data-card="services"] .service-card:hover {
|
||||
background: rgba(255, 255, 255, 0.045);
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
.dash-card[data-card="services"] .service-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.dash-card[data-card="services"] .service-card-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
.dash-card[data-card="services"] .service-card-indicator {
|
||||
font-size: 11px;
|
||||
}
|
||||
.dash-card[data-card="services"] .service-card-status-text,
|
||||
.dash-card[data-card="services"] .service-card-response-time {
|
||||
font-size: 11.5px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
margin: 2px 0;
|
||||
}
|
||||
.dash-card[data-card="services"] .service-card-footer {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.dash-card[data-card="services"] .service-card-button {
|
||||
width: 100%;
|
||||
padding: 6px 10px;
|
||||
font-size: 11.5px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Library card — trim the inner library-status-card chrome since the
|
||||
bento card already provides the framing. */
|
||||
.dash-card[data-card="library"] .library-status-card {
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
.dash-card[data-card="library"] .library-status-glow {
|
||||
display: none;
|
||||
}
|
||||
.dash-card[data-card="library"] .library-status-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.dash-card[data-card="library"] .library-status-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
background: rgba(168, 85, 247, 0.12);
|
||||
border: 1px solid rgba(168, 85, 247, 0.25);
|
||||
color: rgba(168, 85, 247, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dash-card[data-card="library"] .library-status-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.dash-card[data-card="library"] .library-status-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
}
|
||||
.dash-card[data-card="library"] .library-status-subtitle {
|
||||
font-size: 11.5px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
margin: 2px 0 0;
|
||||
}
|
||||
.dash-card[data-card="library"] .library-status-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.dash-card[data-card="library"] .library-status-btn {
|
||||
padding: 6px 10px;
|
||||
font-size: 11.5px;
|
||||
border-radius: 7px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.dash-card[data-card="library"] .library-status-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
.dash-card[data-card="library"] .library-status-stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
background: rgba(255, 255, 255, 0.025);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.dash-card[data-card="library"] .library-status-stat-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 7px;
|
||||
background: rgba(168, 85, 247, 0.10);
|
||||
color: rgba(168, 85, 247, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dash-card[data-card="library"] .library-status-stat-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
.dash-card[data-card="library"] .library-status-stat-value {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
line-height: 1.1;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.dash-card[data-card="library"] .library-status-stat-label {
|
||||
font-size: 10.5px;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Stats card — 2x3 grid of compact metric chips. Override the default
|
||||
stats-grid-dashboard which uses wider, taller cards. */
|
||||
.dash-card[data-card="stats"] .stats-grid-dashboard {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
.dash-card[data-card="stats"] .stat-card-dashboard {
|
||||
background: rgba(255, 255, 255, 0.025);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
box-shadow: none;
|
||||
min-height: 0;
|
||||
}
|
||||
.dash-card[data-card="stats"] .stat-card-dashboard:hover {
|
||||
background: rgba(255, 255, 255, 0.045);
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
.dash-card[data-card="stats"] .stat-card-title {
|
||||
font-size: 10.5px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin: 0;
|
||||
}
|
||||
.dash-card[data-card="stats"] .stat-card-value {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
margin: 4px 0 0;
|
||||
line-height: 1.1;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.dash-card[data-card="stats"] .stat-card-subtitle {
|
||||
font-size: 10.5px;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
/* Activity feed — trim outer chrome */
|
||||
.dash-card[data-card="activity"] .activity-feed-container {
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Sync history strip — horizontal scroller */
|
||||
.dash-card[data-card="syncs"] .sync-history-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 4px 2px;
|
||||
max-height: 260px;
|
||||
}
|
||||
|
||||
.dash-card[data-card="syncs"] .sync-history-card {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
/* Enrichment / rate monitor — single row of compact tiles */
|
||||
.dash-card[data-card="enrichment"] .rate-monitor-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(10, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
padding: 4px 0 0;
|
||||
}
|
||||
|
||||
.dash-card[data-card="enrichment"] .rate-gauge-card {
|
||||
padding: 10px 8px 8px;
|
||||
gap: 4px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.dash-card[data-card="enrichment"] .rate-gauge-svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dash-card[data-card="enrichment"] .gauge-card-header {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dash-card[data-card="enrichment"] .gauge-card-name {
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dash-card[data-card="enrichment"] .gauge-card-status {
|
||||
font-size: 8px;
|
||||
padding: 1px 5px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.dash-card[data-card="enrichment"] .gauge-card-stats {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dash-card[data-card="enrichment"] .gauge-budget-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Active downloads container */
|
||||
.dash-card[data-card="active-downloads"] #dashboard-downloads-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Soft scrollbar inside cards */
|
||||
.dash-card *::-webkit-scrollbar { width: 7px; height: 7px; }
|
||||
.dash-card *::-webkit-scrollbar-track { background: transparent; }
|
||||
.dash-card *::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.08); border-radius: 99px; }
|
||||
.dash-card *::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.18); }
|
||||
|
||||
/* Responsive breakpoints:
|
||||
- >=1500px (large desktop): 3-col bento grid (default)
|
||||
- 1100-1499px (laptop): 2-col bento; full cards span both
|
||||
- 700-1099px (tablet): 2-col bento; tighter sub-grids
|
||||
- <700px (mobile): 1-col stack
|
||||
- <480px (small mobile): tightest padding/typography
|
||||
*/
|
||||
|
||||
/* Laptop — 2-col bento */
|
||||
@media (max-width: 1499px) {
|
||||
.dash-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
.dash-card--wide {
|
||||
grid-column: span 2;
|
||||
}
|
||||
.dash-card--full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.dash-card {
|
||||
padding: 20px 22px;
|
||||
}
|
||||
/* Services sub-grid: 2-up — each service tile keeps room for title + status + button */
|
||||
.dash-card[data-card="services"] .service-status-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.dash-card[data-card="services"] .service-card {
|
||||
padding: 12px 12px;
|
||||
}
|
||||
.dash-card[data-card="services"] .service-card-title {
|
||||
font-size: 12.5px;
|
||||
}
|
||||
.dash-card[data-card="services"] .service-card-status-text,
|
||||
.dash-card[data-card="services"] .service-card-response-time {
|
||||
font-size: 11px;
|
||||
}
|
||||
/* Stats: 3-col stays, value font shrinks slightly */
|
||||
.dash-card[data-card="stats"] .stats-grid-dashboard {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
.dash-card[data-card="stats"] .stat-card-dashboard {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
.dash-card[data-card="stats"] .stat-card-value {
|
||||
font-size: 19px;
|
||||
}
|
||||
.dash-card[data-card="stats"] .stat-card-title,
|
||||
.dash-card[data-card="stats"] .stat-card-subtitle {
|
||||
font-size: 10px;
|
||||
}
|
||||
/* Library stats: 4 across stays in wider 2-col cell */
|
||||
.dash-card[data-card="library"] .library-status-stats {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
/* Enrichment full spans both cols — 10 tiles single row still fits */
|
||||
.dash-card[data-card="enrichment"] .rate-monitor-grid {
|
||||
grid-template-columns: repeat(10, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet — 2-col bento, tighter */
|
||||
@media (max-width: 1099px) {
|
||||
.dash-grid {
|
||||
gap: 14px;
|
||||
}
|
||||
.dash-card {
|
||||
padding: 18px 18px;
|
||||
}
|
||||
.dash-card__title {
|
||||
font-size: 16px;
|
||||
}
|
||||
.dash-card[data-card="services"] .service-status-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
.dash-card[data-card="services"] .service-card {
|
||||
padding: 10px 10px;
|
||||
}
|
||||
.dash-card[data-card="services"] .service-card-button {
|
||||
font-size: 11px;
|
||||
padding: 5px 8px;
|
||||
}
|
||||
.dash-card[data-card="stats"] .stats-grid-dashboard {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
.dash-card[data-card="stats"] .stat-card-value {
|
||||
font-size: 18px;
|
||||
}
|
||||
.dash-card[data-card="library"] .library-status-stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
.dash-card[data-card="enrichment"] .rate-monitor-grid {
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile — 1-col stack */
|
||||
@media (max-width: 699px) {
|
||||
.dash-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
.dash-card--wide,
|
||||
.dash-card--full {
|
||||
grid-column: auto;
|
||||
}
|
||||
.dash-card {
|
||||
padding: 16px 16px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
.dash-card__title {
|
||||
font-size: 15px;
|
||||
}
|
||||
.dash-card__sub {
|
||||
font-size: 12px;
|
||||
}
|
||||
.dash-card[data-card="services"] .service-status-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
.dash-card[data-card="stats"] .stats-grid-dashboard {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
.dash-card[data-card="library"] .library-status-stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
.dash-card[data-card="library"] .library-status-header {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.dash-card[data-card="library"] .library-status-actions {
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
}
|
||||
.dash-card[data-card="enrichment"] .rate-monitor-grid {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
}
|
||||
.dash-card[data-card="enrichment"] .rate-gauge-card {
|
||||
padding: 8px 6px 6px;
|
||||
}
|
||||
.dash-card[data-card="syncs"] .sync-history-cards {
|
||||
max-height: 220px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Small mobile — tightest */
|
||||
@media (max-width: 480px) {
|
||||
.dash-grid {
|
||||
gap: 10px;
|
||||
}
|
||||
.dash-card {
|
||||
padding: 14px 12px;
|
||||
}
|
||||
.dash-card__head {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.dash-card[data-card="stats"] .stats-grid-dashboard {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
.dash-card[data-card="stats"] .stat-card-value {
|
||||
font-size: 20px;
|
||||
}
|
||||
.dash-card[data-card="enrichment"] .rate-monitor-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
.dash-card[data-card="enrichment"] .gauge-card-name {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue