From 60d737f7ab89d477a3a0b53b8dded6173271be93 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Thu, 16 Apr 2026 08:11:18 -0700 Subject: [PATCH] Add Tools sidebar page with grouped layout and Library Maintenance hero MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dashboard Tools & Operations section replaced with a compact link card. All 10 tool cards moved to a dedicated Tools page in the sidebar, grouped into three sections: Database & Scanning, Metadata & Cache, Management. Library Maintenance promoted to hero position at the top of the page with accent top bar, logo, enable toggle, and tabbed content (Jobs, Findings, History) — no longer buried in a modal. openRepairModal() now navigates to the Tools page. Repair modal HTML removed. Tool initialization extracted from loadDashboardData() into a dedicated initializeToolsPage() with idempotent event listener wiring. Container sizing updated to use margin: 20px (matching Dashboard/Stats) instead of max-width: 1400px for consistent full-width appearance across all pages. --- webui/index.html | 855 +++++++++++++++++++++-------------------- webui/static/helper.js | 1 + webui/static/script.js | 106 +++-- webui/static/style.css | 226 ++++++++++- 4 files changed, 721 insertions(+), 467 deletions(-) diff --git a/webui/index.html b/webui/index.html index d42fa59d..cde26366 100644 --- a/webui/index.html +++ b/webui/index.html @@ -239,6 +239,10 @@ Library + - -

Last Full Refresh: Never

-
-
- Artists: - 0 -
-
- Albums: - 0 -
-
- Tracks: - 0 -
-
- Size: - 0.0 MB -
-
-
- - -
-
-

Idle

-
-
-
-

0 / 0 artists (0.0%)

-
- - -
-
-

Metadata Updater

- -
-

Updates artist photos, genres, - and album art from Spotify.

-
- - -
-
-

Current Artist: Not - running

-
-
-
-
-

0 / 0 artists (0.0%) -

-
-
- -
-
-

Quality Scanner

- -
-

Scan library for tracks below quality preferences

-
-
- Processed: - 0 -
-
- Quality Met: - 0 -
-
- Low Quality: - 0 -
-
- Matched: - 0 -
-
-
- - -
-
-

Ready to scan

-
-
-
-
-

0 / 0 tracks scanned - (0.0%)

-
-
- -
-
-

Duplicate Cleaner

- -
-

Detect and remove duplicate tracks in Transfer folder

-
-
- Files Scanned: - 0 -
-
- Duplicates Found: - 0 -
-
- Deleted: - 0 -
-
- Space Freed: - 0 MB -
-
-
- -
-
-

Ready to scan

-
-
-
-
-

0 files scanned - (0.0%)

-
-
- -
-
-

Discovery Pool

-
-

View and fix matched/failed discovery results across all mirrored playlists

-
-
- Matched: - -
-
- Failed: - -
-
-
- -
-
- -
-
-

Retag Tool

- -
-

Fix metadata on previously downloaded albums & singles

-
-
- Groups: - 0 -
-
- Tracks: - 0 -
-
- Artists: - 0 -
-
- Status: - Idle -
-
-
- -
-
-

Ready

-
-
-
-
-

0 / 0 tracks (0.0%)

-
-
- - - -
-
-

Backup Manager

- -
-

Create, download, restore and manage database backups

-
-
- Last Backup: - Never -
-
- Backups: - 0 -
-
- Latest Size: - -
-
- DB Size: - -
-
-
- -
-
-
- - -
-
-

Metadata Cache

- -
-

Cached API responses from Spotify & iTunes

-
-
- Artists: - 0 -
-
- Albums: - 0 -
-
- Tracks: - 0 -
-
- Hits: - 0 -
-
-
- - -
-
- - -
-
-

Download Blacklist

-
-

Blocked sources that won't be used for future downloads

-
-
- Blocked: - 0 -
-
-
- +
+

Recent Activity

@@ -6411,6 +6098,443 @@
+ +
+
+
+
+

+ + + + Tools & Operations +

+

Database management, library scanning, metadata, backups

+
+
+ + +
+
+
+ +
+

Library Maintenance

+

Automated scanning, detection, and repair of library issues

+
+
+ +
+ +
+ + + +
+ +
+
+
Loading jobs...
+
+
+ + + + +
+ + +
+

Database & Scanning

+
+
+
+

Database Updater

+ +
+

Last Full Refresh: Never

+
+
+ Artists: + 0 +
+
+ Albums: + 0 +
+
+ Tracks: + 0 +
+
+ Size: + 0.0 MB +
+
+
+ + +
+
+

Idle

+
+
+
+

0 / 0 artists (0.0%)

+
+
+ +
+
+

Metadata Updater

+ +
+ +
+ + +
+
+

Current Artist: Not + running

+
+
+
+
+

0 / 0 artists (0.0%) +

+
+
+ +
+
+

Quality Scanner

+ +
+

Scan library for tracks below quality preferences

+
+
+ Processed: + 0 +
+
+ Quality Met: + 0 +
+
+ Low Quality: + 0 +
+
+ Matched: + 0 +
+
+
+ + +
+
+

Ready to scan

+
+
+
+
+

0 / 0 tracks scanned + (0.0%)

+
+
+ +
+
+

Duplicate Cleaner

+ +
+

Detect and remove duplicate tracks in Transfer folder

+
+
+ Files Scanned: + 0 +
+
+ Duplicates Found: + 0 +
+
+ Deleted: + 0 +
+
+ Space Freed: + 0 MB +
+
+
+ +
+
+

Ready to scan

+
+
+
+
+

0 files scanned + (0.0%)

+
+
+ + + +
+ + +
+

Metadata & Cache

+
+ +
+
+

Discovery Pool

+
+

View and fix matched/failed discovery results across all mirrored playlists

+
+
+ Matched: + +
+
+ Failed: + +
+
+
+ +
+
+ +
+
+

Retag Tool

+ +
+

Fix metadata on previously downloaded albums & singles

+
+
+ Groups: + 0 +
+
+ Tracks: + 0 +
+
+ Artists: + 0 +
+
+ Status: + Idle +
+
+
+ +
+
+

Ready

+
+
+
+
+

0 / 0 tracks (0.0%)

+
+
+ +
+ + +
+

Management

+
+ +
+
+

Backup Manager

+ +
+

Create, download, restore and manage database backups

+
+
+ Last Backup: + Never +
+
+ Backups: + 0 +
+
+ Latest Size: + +
+
+ DB Size: + +
+
+
+ +
+
+
+ + +
+
+

Metadata Cache

+ +
+

Cached API responses from Spotify & iTunes

+
+
+ Artists: + 0 +
+
+ Albums: + 0 +
+
+ Tracks: + 0 +
+
+ Hits: + 0 +
+
+
+ + +
+
+ + +
+
+

Download Blacklist

+
+

Blocked sources that won't be used for future downloads

+
+
+ Blocked: + 0 +
+
+
+ +
+
+
+
+
+ @@ -7497,89 +7621,6 @@
- - -
diff --git a/webui/static/helper.js b/webui/static/helper.js index 5a98a179..964ebd93 100644 --- a/webui/static/helper.js +++ b/webui/static/helper.js @@ -3602,6 +3602,7 @@ const WHATS_NEW = { '2.2': [ // --- April 15, 2026 --- { date: 'April 15, 2026' }, + { 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' }, { title: 'Fix Spotify API Leaking When Deezer/iTunes is Primary', desc: 'Spotify was being called for watchlist album scanning, similar artist discovery, repair jobs, and the Artists page search even when another source was set as primary. All data-fetching now respects the configured primary source. Spotify playlist sync is unaffected' }, diff --git a/webui/static/script.js b/webui/static/script.js index 7e050bc0..b5c34170 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -3048,6 +3048,9 @@ async function loadPageData(pageId) { // Load comparisons loadHydrabaseComparisons(); break; + case 'tools': + await initializeToolsPage(); + break; case 'watchlist': await initializeWatchlistPage(); break; @@ -25088,80 +25091,81 @@ function resetWishlistModalToIdleState() { } } -async function loadDashboardData() { - // Attach event listeners for the DB updater tool +let toolsPageState = { isInitialized: false }; + +async function initializeToolsPage() { + // Attach event listeners for tool buttons (idempotent — getElementById returns null if already wired) const updateButton = document.getElementById('db-update-button'); - if (updateButton) { + if (updateButton && !updateButton._toolsWired) { updateButton.addEventListener('click', handleDbUpdateButtonClick); + updateButton._toolsWired = true; } - // Attach event listeners for the metadata updater tool const metadataButton = document.getElementById('metadata-update-button'); - if (metadataButton) { + if (metadataButton && !metadataButton._toolsWired) { metadataButton.addEventListener('click', handleMetadataUpdateButtonClick); + metadataButton._toolsWired = true; } - // Check active media server and hide metadata updater if not Plex - await checkAndHideMetadataUpdaterForNonPlex(); - - // Check for ongoing metadata update and restore state - await checkAndRestoreMetadataUpdateState(); - - // Attach event listener for the quality scanner tool const qualityScanButton = document.getElementById('quality-scan-button'); - if (qualityScanButton) { + if (qualityScanButton && !qualityScanButton._toolsWired) { qualityScanButton.addEventListener('click', handleQualityScanButtonClick); + qualityScanButton._toolsWired = true; } - // Attach event listener for the duplicate cleaner tool const duplicateCleanButton = document.getElementById('duplicate-clean-button'); - if (duplicateCleanButton) { + if (duplicateCleanButton && !duplicateCleanButton._toolsWired) { duplicateCleanButton.addEventListener('click', handleDuplicateCleanButtonClick); + duplicateCleanButton._toolsWired = true; } - // Attach event listener for the retag tool const retagOpenButton = document.getElementById('retag-open-button'); - if (retagOpenButton) { + if (retagOpenButton && !retagOpenButton._toolsWired) { retagOpenButton.addEventListener('click', openRetagModal); + retagOpenButton._toolsWired = true; } - // Attach event listener for the media scan tool const mediaScanButton = document.getElementById('media-scan-button'); - if (mediaScanButton) { + if (mediaScanButton && !mediaScanButton._toolsWired) { mediaScanButton.addEventListener('click', handleMediaScanButtonClick); + mediaScanButton._toolsWired = true; } - // Check active media server and show media scan tool only for Plex - await checkAndShowMediaScanForPlex(); - - // Attach event listener for the backup manager const backupNowButton = document.getElementById('backup-now-button'); - if (backupNowButton) backupNowButton.addEventListener('click', handleBackupNowClick); - loadBackupList(); + if (backupNowButton && !backupNowButton._toolsWired) { + backupNowButton.addEventListener('click', handleBackupNowClick); + backupNowButton._toolsWired = true; + } - // Attach event listeners for tool help buttons + // Tool-specific init + await checkAndHideMetadataUpdaterForNonPlex(); + await checkAndRestoreMetadataUpdateState(); + await checkAndShowMediaScanForPlex(); + loadBackupList(); initializeToolHelpButtons(); - - // Initial load of retag stats loadRetagStats(); - - // Check for ongoing retag operation checkRetagStatus(); - - // Initial load of stats await fetchAndUpdateDbStats(); + loadDiscoveryPoolStats(); + loadMetadataCacheStats(); - // Start periodic refresh of stats (every 10 seconds) - stopDbStatsPolling(); // Ensure no duplicates + // Start polling (cleared when navigating away via loadPageData preamble) + stopDbStatsPolling(); dbStatsInterval = setInterval(fetchAndUpdateDbStats, 10000); - // Initial load of discovery pool stats for the tool card - loadDiscoveryPoolStats(); + // Check for ongoing operations + await checkAndUpdateDbProgress(); + await checkAndUpdateQualityScanProgress(); + await checkAndUpdateDuplicateCleanProgress(); - // Initial load of metadata cache stats + periodic refresh - loadMetadataCacheStats(); - setInterval(loadMetadataCacheStats, 15000); + // Initialize library maintenance section + updateRepairStatus(); + switchRepairTab('jobs'); + toolsPageState.isInitialized = true; +} + +async function loadDashboardData() { // Initial load of wishlist count await updateWishlistCount(); @@ -25186,15 +25190,6 @@ async function loadDashboardData() { // Start periodic toast checking (every 3 seconds) setInterval(checkForActivityToasts, 3000); - // Also check the status of any ongoing update when the page loads - await checkAndUpdateDbProgress(); - - // Check for any ongoing quality scanner when the page loads - await checkAndUpdateQualityScanProgress(); - - // Check for any ongoing duplicate cleaner when the page loads - await checkAndUpdateDuplicateCleanProgress(); - // Check for any active download processes that need rehydration await checkForActiveProcesses(); @@ -63191,10 +63186,12 @@ let _repairJobsCache = {}; // Cache job data for help modal * Open the Library Maintenance modal */ async function openRepairModal() { - const modal = document.getElementById('repair-modal'); - if (!modal) return; - modal.style.display = 'flex'; - document.body.style.overflow = 'hidden'; + navigateToPage('tools'); + // Scroll to maintenance section + setTimeout(() => { + const section = document.querySelector('.tools-maintenance-section'); + if (section) section.scrollIntoView({ behavior: 'smooth' }); + }, 100); _repairCurrentTab = 'jobs'; switchRepairTab('jobs'); // Load master toggle state @@ -63213,10 +63210,7 @@ async function openRepairModal() { } function closeRepairModal() { - const modal = document.getElementById('repair-modal'); - if (!modal) return; - modal.style.display = 'none'; - document.body.style.overflow = ''; + // No-op — repair content now lives on the tools page, no modal to close } async function toggleRepairMaster() { diff --git a/webui/static/style.css b/webui/static/style.css index 17a06e66..d24f99f7 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -55797,14 +55797,232 @@ body.reduce-effects *::after { } } +/* ═══════════════════════════════════════════════════════════════════ + DASHBOARD TOOLS LINK CARD + ═══════════════════════════════════════════════════════════════════ */ + +.dashboard-tools-link { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px; + background: rgba(255, 255, 255, 0.025); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 12px; + cursor: pointer; + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); +} + +.dashboard-tools-link:hover { + background: rgba(255, 255, 255, 0.05); + border-color: rgba(var(--accent-rgb), 0.15); + transform: translateY(-1px); +} + +.dashboard-tools-link-content { + display: flex; + align-items: center; + gap: 12px; +} + +.dashboard-tools-link-title { + font-size: 14px; + color: rgba(255, 255, 255, 0.6); + font-weight: 500; +} + +/* ═══════════════════════════════════════════════════════════════════ + TOOLS PAGE + ═══════════════════════════════════════════════════════════════════ */ + +.tools-page-container { + padding: 28px 24px 30px; + margin: 20px; + background: linear-gradient(135deg, + rgba(20, 20, 20, 0.55) 0%, + rgba(12, 12, 12, 0.62) 100%); + border-radius: 24px; + border: 1px solid rgba(255, 255, 255, 0.08); + border-top: 1px solid rgba(255, 255, 255, 0.12); + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.3), + 0 4px 16px rgba(0, 0, 0, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.08); +} + +.tools-page-header { + padding: 20px 24px 18px; + margin: -28px -24px 24px -24px; + background: linear-gradient(180deg, + rgba(var(--accent-rgb), 0.08) 0%, + rgba(var(--accent-rgb), 0.03) 40%, + transparent 100%); + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + border-top-left-radius: 24px; + border-top-right-radius: 24px; + position: relative; +} + +.tools-page-header::after { + content: ''; + position: absolute; + bottom: -1px; + left: 10%; + right: 10%; + height: 1px; + background: linear-gradient(90deg, + transparent 0%, + rgba(var(--accent-rgb), 0.2) 20%, + rgba(var(--accent-rgb), 0.35) 50%, + rgba(var(--accent-rgb), 0.2) 80%, + transparent 100%); +} + +.tools-page-title { + display: flex; + align-items: center; + gap: 10px; + font-size: 26px; + font-weight: 700; + color: #fff; + margin: 0; + letter-spacing: -0.5px; + font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif; +} + +.tools-page-subtitle { + font-size: 13px; + color: rgba(255, 255, 255, 0.4); + margin: 6px 0 0 0; + font-weight: 500; +} + +/* ── Library Maintenance hero section ── */ +.tools-maintenance-hero { + background: linear-gradient(135deg, + rgba(20, 20, 20, 0.95) 0%, + rgba(12, 12, 12, 0.98) 100%); + border: 1px solid rgba(255, 255, 255, 0.08); + border-top: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 16px; + padding: 20px 24px; + margin-bottom: 28px; + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.06); + position: relative; + overflow: hidden; +} + +.tools-maintenance-hero::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, + transparent, + rgba(var(--accent-rgb), 0.5), + rgba(var(--accent-rgb), 0.8), + rgba(var(--accent-rgb), 0.5), + transparent); + box-shadow: 0 0 12px rgba(var(--accent-rgb), 0.3); +} + +.tools-maintenance-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} + +.tools-maintenance-header-left { + display: flex; + align-items: center; + gap: 14px; +} + +.tools-maintenance-logo { + width: 40px; + height: 40px; + border-radius: 10px; + object-fit: cover; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); +} + +.tools-maintenance-title { + font-size: 20px; + font-weight: 700; + color: #fff; + margin: 0 0 2px 0; + letter-spacing: -0.3px; +} + +.tools-maintenance-subtitle { + font-size: 12px; + color: rgba(255, 255, 255, 0.4); + margin: 0; + font-weight: 500; +} + +/* Repair tabs on the tools page */ +.tools-maintenance-hero .repair-tabs { + margin-bottom: 16px; +} + +.tools-maintenance-hero .repair-tab-content { + min-height: 200px; +} + +/* ── Tool card sections ── */ +.tools-section { + margin-bottom: 24px; +} + +.tools-section-title { + font-size: 13px; + font-weight: 600; + color: rgba(255, 255, 255, 0.35); + text-transform: uppercase; + letter-spacing: 1.2px; + margin: 0 0 12px 4px; + padding-bottom: 8px; + border-bottom: 1px solid rgba(255, 255, 255, 0.04); +} + +@media (max-width: 768px) { + .tools-page-container { + padding: 16px; + margin: 10px; + border-radius: 16px; + } + + .tools-page-header { + padding: 16px; + margin: -16px -16px 16px -16px; + border-top-left-radius: 16px; + border-top-right-radius: 16px; + } + + .tools-page-title { + font-size: 22px; + } + + .tools-maintenance-header { + flex-direction: column; + align-items: flex-start; + gap: 12px; + } +} + /* ═══════════════════════════════════════════════════════════════════ WATCHLIST PAGE ═══════════════════════════════════════════════════════════════════ */ .watchlist-page-container { padding: 28px 24px 30px; - max-width: 1400px; - margin: 0 auto; + margin: 20px; background: linear-gradient(135deg, rgba(20, 20, 20, 0.55) 0%, rgba(12, 12, 12, 0.62) 100%); @@ -56216,8 +56434,7 @@ body.reduce-effects *::after { .wishlist-page-container { padding: 28px 24px 30px; - max-width: 1400px; - margin: 0 auto; + margin: 20px; background: linear-gradient(135deg, rgba(20, 20, 20, 0.55) 0%, rgba(12, 12, 12, 0.62) 100%); @@ -56534,6 +56751,7 @@ body.reduce-effects *::after { .watchlist-page-container, .wishlist-page-container { padding: 16px; + margin: 10px; border-radius: 16px; }