diff --git a/packages/frontend/src/components/SongMatching.tsx b/packages/frontend/src/components/SongMatching.tsx index 3ead7f5..920e39a 100644 --- a/packages/frontend/src/components/SongMatching.tsx +++ b/packages/frontend/src/components/SongMatching.tsx @@ -11,10 +11,6 @@ import { CardHeader, Badge, Progress, - Alert, - AlertIcon, - AlertTitle, - AlertDescription, useToast, Modal, ModalOverlay, @@ -57,7 +53,6 @@ export const SongMatching: React.FC = () => { const [stats, setStats] = useState(null); const [unmatchedMusicFiles, setUnmatchedMusicFiles] = useState([]); const [matchedMusicFiles, setMatchedMusicFiles] = useState([]); - const [songsWithoutMusicFiles, setSongsWithoutMusicFiles] = useState([]); const [songsWithMusicFiles, setSongsWithMusicFiles] = useState([]); const [isLoading, setIsLoading] = useState(false); const [autoLinking, setAutoLinking] = useState(false); @@ -75,12 +70,11 @@ export const SongMatching: React.FC = () => { const loadData = async () => { setIsLoading(true); try { - const [statsRes, unmatchedRes, matchedRes, songsWithoutRes, songsWithRes] = await Promise.all([ + const [statsRes, unmatchedRes, matchedRes, songsWithRes] = await Promise.all([ fetch('/api/matching/stats'), - fetch('/api/matching/unmatched-music-files'), - fetch('/api/matching/matched-music-files'), - fetch('/api/matching/songs-without-music-files'), - fetch('/api/matching/songs-with-music-files') + fetch('/api/matching/unmatched-music-files?limit=200'), + fetch('/api/matching/matched-music-files?limit=200'), + fetch('/api/matching/songs-with-music-files?limit=200') ]); if (statsRes.ok) { @@ -98,11 +92,6 @@ export const SongMatching: React.FC = () => { setMatchedMusicFiles(matchedData.musicFiles); } - if (songsWithoutRes.ok) { - const songsData = await songsWithoutRes.json(); - setSongsWithoutMusicFiles(songsData.songs); - } - if (songsWithRes.ok) { const songsData = await songsWithRes.json(); setSongsWithMusicFiles(songsData.songs); @@ -257,12 +246,7 @@ export const SongMatching: React.FC = () => { return 'red'; }; - const formatDuration = (seconds: number): string => { - if (!seconds || isNaN(seconds)) return '00:00'; - const mins = Math.floor(seconds / 60); - const secs = Math.floor(seconds % 60); - return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; - }; + const canAutoLink = !!stats && stats.unmatchedMusicFiles > 0; if (isLoading) { return ( @@ -315,7 +299,7 @@ export const SongMatching: React.FC = () => { - Automatically match and link music files to songs in your Rekordbox library + Try linking any remaining unmatched files. The main S3 sync already performs matching; use this for leftovers. diff --git a/packages/frontend/src/pages/Configuration.tsx b/packages/frontend/src/pages/Configuration.tsx index c533279..4aaf24e 100644 --- a/packages/frontend/src/pages/Configuration.tsx +++ b/packages/frontend/src/pages/Configuration.tsx @@ -35,7 +35,7 @@ import { S3Configuration } from "./S3Configuration"; import { MusicUpload } from "../components/MusicUpload"; import { SongMatching } from "../components/SongMatching"; import { api } from "../services/api"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; interface MusicFile { _id: string; @@ -60,11 +60,52 @@ export function Configuration() { const [musicFiles, setMusicFiles] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isSyncing, setIsSyncing] = useState(false); + const [hasS3Config, setHasS3Config] = useState(false); + const [musicLoaded, setMusicLoaded] = useState(false); - // Load music files on component mount - useEffect(() => { - loadMusicFiles(); + // Tabs: remember active tab across refreshes + const initialTabIndex = useMemo(() => { + const stored = localStorage.getItem('configTabIndex'); + return stored ? parseInt(stored, 10) : 0; }, []); + const [tabIndex, setTabIndex] = useState(initialTabIndex); + + // Tab indices (keep in sync with Tab order below) + const TAB_INDEX = { + LIBRARY: 0, + UPLOAD: 1, + MUSIC_LIBRARY: 2, + MATCHING: 3, + S3_CONFIG: 4, + } as const; + + // Fetch S3 config (small and safe to do on mount) + useEffect(() => { + const fetchS3Config = async () => { + try { + const res = await fetch('/api/config/s3'); + if (res.ok) { + const data = await res.json(); + const cfg = data?.config || {}; + const required = ['endpoint', 'region', 'accessKeyId', 'secretAccessKey', 'bucketName']; + const present = required.every((k) => typeof cfg[k] === 'string' && cfg[k]); + setHasS3Config(present); + } else { + setHasS3Config(false); + } + } catch { + setHasS3Config(false); + } + }; + fetchS3Config(); + }, []); + + // Lazy-load: only load music files when Music Library tab becomes active + useEffect(() => { + if (tabIndex === TAB_INDEX.MUSIC_LIBRARY && !musicLoaded) { + loadMusicFiles().then(() => setMusicLoaded(true)); + } + }, [tabIndex]); const loadMusicFiles = async () => { setIsLoading(true); @@ -107,8 +148,13 @@ export function Configuration() { isClosable: true, }); - // Reload music files to show the new ones - await loadMusicFiles(); + // Reload music files to show the new ones if user is on Music Library tab + if (tabIndex === TAB_INDEX.MUSIC_LIBRARY) { + await loadMusicFiles(); + } else { + // Mark as not loaded so that when user opens the tab, it fetches fresh + setMusicLoaded(false); + } } else { throw new Error('Failed to sync S3'); } @@ -241,7 +287,16 @@ export function Configuration() { Configuration - + { + setTabIndex(index); + localStorage.setItem('configTabIndex', String(index)); + }} + > @@ -351,6 +406,7 @@ export function Configuration() { colorScheme="blue" onClick={handleSyncS3} isLoading={isSyncing} + isDisabled={!hasS3Config || isSyncing} loadingText="Syncing..." _hover={{ bg: "blue.900", borderColor: "blue.400" }} > @@ -359,11 +415,11 @@ export function Configuration() { - {isLoading ? ( + {tabIndex === TAB_INDEX.MUSIC_LIBRARY && isLoading ? ( Loading music files... - ) : musicFiles.length === 0 ? ( + ) : tabIndex === TAB_INDEX.MUSIC_LIBRARY && musicFiles.length === 0 ? ( No music files found in the database. @@ -373,12 +429,18 @@ export function Configuration() { leftIcon={} onClick={handleSyncS3} isLoading={isSyncing} + isDisabled={!hasS3Config || isSyncing} loadingText="Syncing..." colorScheme="blue" _hover={{ bg: "blue.700" }} > Sync S3 Bucket + {!hasS3Config && ( + + Configure your S3 connection in the S3 Configuration tab first. + + )} ) : (