Jelajahi Sumber

Consolidate cache-all-previews logic and fix Docker restart

- Extract shared cacheAllPreviews() function to previewCache.ts
- Simplify BrowsePage and Layout modal to use shared function
- Fix PlaylistsPage to validate previews before caching
- Fix Docker restart using correct container name (dune-weaver-backend)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
tuanchris 3 minggu lalu
induk
melakukan
a27fae0282

+ 12 - 61
frontend/src/components/layout/Layout.tsx

@@ -3,7 +3,7 @@ import { useEffect, useState, useRef } from 'react'
 import { toast } from 'sonner'
 import { NowPlayingBar } from '@/components/NowPlayingBar'
 import { Button } from '@/components/ui/button'
-import { initPreviewCacheDB, getPreviewsFromCache, savePreviewToCache } from '@/lib/previewCache'
+import { cacheAllPreviews } from '@/lib/previewCache'
 
 const navItems = [
   { path: '/', label: 'Browse', icon: 'grid_view', title: 'Browse Patterns' },
@@ -516,67 +516,17 @@ export function Layout() {
   const handleCacheAllPreviews = async () => {
     setCacheAllProgress({ inProgress: true, completed: 0, total: 0, done: false })
 
-    try {
-      // Initialize IndexedDB
-      await initPreviewCacheDB()
-
-      // Fetch all patterns
-      const response = await fetch('/api/patterns')
-      const data = await response.json()
-      const patterns: { file: string }[] = data.patterns || []
-      const allPaths = patterns.map((p) => p.file)
-
-      // Check which patterns are already cached
-      const cachedPreviews = await getPreviewsFromCache(allPaths)
-      const uncachedPatterns = allPaths.filter((path) => !cachedPreviews.has(path))
+    const result = await cacheAllPreviews((progress) => {
+      setCacheAllProgress({ inProgress: !progress.done, ...progress })
+    })
 
-      if (uncachedPatterns.length === 0) {
+    if (result.success) {
+      if (result.cached === 0) {
         toast.success('All patterns are already cached!')
-        setCacheAllProgress({ inProgress: false, completed: patterns.length, total: patterns.length, done: true })
-        return
-      }
-
-      setCacheAllProgress({ inProgress: true, completed: 0, total: uncachedPatterns.length, done: false })
-
-      // Process in batches of 5
-      const batchSize = 5
-      let completed = 0
-
-      for (let i = 0; i < uncachedPatterns.length; i += batchSize) {
-        const batch = uncachedPatterns.slice(i, i + batchSize)
-
-        const batchPromises = batch.map(async (patternPath: string) => {
-          try {
-            // Fetch preview data
-            const previewResponse = await fetch(
-              `/api/pattern/${encodeURIComponent(patternPath)}/preview`
-            )
-            if (previewResponse.ok) {
-              const previewData = await previewResponse.json()
-              if (previewData.image_data) {
-                // Save to IndexedDB cache
-                await savePreviewToCache(patternPath, previewData)
-              }
-            }
-          } catch {
-            // Continue even if one fails
-          }
-        })
-
-        await Promise.all(batchPromises)
-        completed += batch.length
-        setCacheAllProgress({ inProgress: true, completed, total: uncachedPatterns.length, done: false })
-
-        // Small delay between batches
-        if (i + batchSize < uncachedPatterns.length) {
-          await new Promise((resolve) => setTimeout(resolve, 100))
-        }
+      } else {
+        toast.success(`Cached ${result.cached} pattern previews`)
       }
-
-      setCacheAllProgress({ inProgress: false, completed: uncachedPatterns.length, total: uncachedPatterns.length, done: true })
-      toast.success(`Cached ${uncachedPatterns.length} pattern previews`)
-    } catch (error) {
-      console.error('Error caching previews:', error)
+    } else {
       setCacheAllProgress(null)
       toast.error('Failed to cache previews')
     }
@@ -678,8 +628,9 @@ export function Layout() {
                     <Button variant="ghost" onClick={handleSkipCacheAll}>
                       Skip for now
                     </Button>
-                    <Button onClick={handleCacheAllPreviews}>
-                      Cache All Previews
+                    <Button variant="outline" onClick={handleCacheAllPreviews} className="gap-2">
+                      <span className="material-icons-outlined text-lg">cached</span>
+                      Cache All
                     </Button>
                   </div>
                 )}

+ 74 - 0
frontend/src/lib/previewCache.ts

@@ -311,3 +311,77 @@ export async function getPreviewsFromCache(
 
   return results
 }
+
+// Shared function to cache all previews - used by both BrowsePage and Layout modal
+export interface CacheAllProgress {
+  completed: number
+  total: number
+  done: boolean
+}
+
+export async function cacheAllPreviews(
+  onProgress: (progress: CacheAllProgress) => void
+): Promise<{ success: boolean; cached: number }> {
+  const BATCH_SIZE = 10
+
+  try {
+    await initPreviewCacheDB()
+
+    // Fetch all patterns
+    const response = await fetch('/list_theta_rho_files_with_metadata')
+    const patterns: { path: string }[] = await response.json()
+    const allPaths = patterns.map((p) => p.path)
+
+    // Check which patterns are already cached
+    const cachedPreviews = await getPreviewsFromCache(allPaths)
+    const uncachedPatterns = allPaths.filter((path) => !cachedPreviews.has(path))
+
+    if (uncachedPatterns.length === 0) {
+      onProgress({ completed: patterns.length, total: patterns.length, done: true })
+      return { success: true, cached: 0 }
+    }
+
+    onProgress({ completed: 0, total: uncachedPatterns.length, done: false })
+
+    const totalBatches = Math.ceil(uncachedPatterns.length / BATCH_SIZE)
+
+    for (let i = 0; i < totalBatches; i++) {
+      const batchStart = i * BATCH_SIZE
+      const batchEnd = Math.min(batchStart + BATCH_SIZE, uncachedPatterns.length)
+      const batchPatterns = uncachedPatterns.slice(batchStart, batchEnd)
+
+      try {
+        const batchResponse = await fetch('/preview_thr_batch', {
+          method: 'POST',
+          headers: { 'Content-Type': 'application/json' },
+          body: JSON.stringify({ file_names: batchPatterns }),
+        })
+
+        if (batchResponse.ok) {
+          const results = await batchResponse.json()
+
+          for (const [path, data] of Object.entries(results)) {
+            if (data && !(data as { error?: string }).error) {
+              await savePreviewToCache(path, data as PreviewData)
+            }
+          }
+        }
+      } catch {
+        // Continue even if batch fails
+      }
+
+      onProgress({ completed: batchEnd, total: uncachedPatterns.length, done: false })
+
+      // Small delay between batches
+      if (i + 1 < totalBatches) {
+        await new Promise((resolve) => setTimeout(resolve, 100))
+      }
+    }
+
+    onProgress({ completed: uncachedPatterns.length, total: uncachedPatterns.length, done: true })
+    return { success: true, cached: uncachedPatterns.length }
+  } catch (error) {
+    console.error('Error caching previews:', error)
+    return { success: false, cached: 0 }
+  }
+}

+ 17 - 104
frontend/src/pages/BrowsePage.tsx

@@ -4,6 +4,7 @@ import {
   initPreviewCacheDB,
   getPreviewsFromCache,
   savePreviewToCache,
+  cacheAllPreviews,
 } from '@/lib/previewCache'
 import { fuzzyMatch } from '@/lib/utils'
 import { useOnBackendConnected } from '@/hooks/useBackendConnection'
@@ -97,7 +98,6 @@ export function BrowsePage() {
   const [isCaching, setIsCaching] = useState(false)
   const [cacheProgress, setCacheProgress] = useState(0)
   const [allCached, setAllCached] = useState(false)
-  const cacheAbortRef = useRef(false)
 
   // Favorites state
   const [favorites, setFavorites] = useState<Set<string>>(new Set())
@@ -707,118 +707,31 @@ export function BrowsePage() {
 
   // Cache all previews handler
   const handleCacheAllPreviews = async () => {
-    if (isCaching) {
-      // Cancel caching
-      cacheAbortRef.current = true
-      return
-    }
-
-    const BATCH_SIZE = 10
-    const CACHE_PROGRESS_KEY = 'dune_weaver_cache_progress'
-    const CACHE_TIMESTAMP_KEY = 'dune_weaver_cache_timestamp'
-    const CACHE_PROGRESS_EXPIRY = 24 * 60 * 60 * 1000 // 24 hours
+    if (isCaching) return
 
     setIsCaching(true)
     setCacheProgress(0)
-    cacheAbortRef.current = false
 
-    try {
-      // Check IndexedDB cache for all patterns to find uncached ones
-      const allPaths = patterns.map((p) => p.path)
-      const cachedPreviews = await getPreviewsFromCache(allPaths)
-      const uncachedPatterns = allPaths.filter((path) => !cachedPreviews.has(path))
+    const result = await cacheAllPreviews((progress) => {
+      const percentage = progress.total > 0
+        ? Math.round((progress.completed / progress.total) * 100)
+        : 0
+      setCacheProgress(percentage)
+    })
 
-      if (uncachedPatterns.length === 0) {
+    if (result.success) {
+      setAllCached(true)
+      if (result.cached === 0) {
         toast.success('All patterns are already cached!')
-        setAllCached(true)
-        setIsCaching(false)
-        return
-      }
-
-      // Check for existing progress
-      let startIndex = 0
-      const savedProgress = localStorage.getItem(CACHE_PROGRESS_KEY)
-      const savedTimestamp = localStorage.getItem(CACHE_TIMESTAMP_KEY)
-
-      if (savedProgress && savedTimestamp) {
-        const progressAge = Date.now() - parseInt(savedTimestamp)
-        if (progressAge < CACHE_PROGRESS_EXPIRY) {
-          const lastIndex = uncachedPatterns.findIndex((p) => p === savedProgress)
-          if (lastIndex !== -1) {
-            startIndex = lastIndex + 1
-            toast.info('Resuming from previous progress...')
-          }
-        } else {
-          localStorage.removeItem(CACHE_PROGRESS_KEY)
-          localStorage.removeItem(CACHE_TIMESTAMP_KEY)
-        }
-      }
-
-      const remainingPatterns = uncachedPatterns.slice(startIndex)
-      const totalBatches = Math.ceil(remainingPatterns.length / BATCH_SIZE)
-
-      for (let i = 0; i < totalBatches; i++) {
-        if (cacheAbortRef.current) {
-          toast.info('Caching cancelled')
-          break
-        }
-
-        const batchStart = i * BATCH_SIZE
-        const batchEnd = Math.min(batchStart + BATCH_SIZE, remainingPatterns.length)
-        const batchPatterns = remainingPatterns.slice(batchStart, batchEnd)
-
-        // Update progress
-        const overallProgress = Math.round(
-          ((startIndex + batchEnd) / uncachedPatterns.length) * 100
-        )
-        setCacheProgress(overallProgress)
-
-        try {
-          const response = await fetch('/preview_thr_batch', {
-            method: 'POST',
-            headers: { 'Content-Type': 'application/json' },
-            body: JSON.stringify({ file_names: batchPatterns }),
-          })
-
-          if (response.ok) {
-            const results = await response.json()
-
-            // Cache each preview and update state
-            for (const [path, data] of Object.entries(results)) {
-              if (data && !(data as PreviewData).error) {
-                await savePreviewToCache(path, data as PreviewData)
-
-                // Save progress after each successful pattern
-                localStorage.setItem(CACHE_PROGRESS_KEY, path)
-                localStorage.setItem(CACHE_TIMESTAMP_KEY, Date.now().toString())
-              }
-            }
-
-            // Update previews state
-            setPreviews((prev) => ({ ...prev, ...results }))
-          }
-        } catch (error) {
-          console.error(`Error caching batch ${i + 1}:`, error)
-        }
-
-        // Small delay between batches
-        await new Promise((resolve) => setTimeout(resolve, 100))
-      }
-
-      if (!cacheAbortRef.current) {
-        // Clear progress after successful completion
-        localStorage.removeItem(CACHE_PROGRESS_KEY)
-        localStorage.removeItem(CACHE_TIMESTAMP_KEY)
-        setAllCached(true)
+      } else {
         toast.success('All pattern previews have been cached!')
       }
-    } catch (error) {
-      console.error('Error caching previews:', error)
-      toast.error('Failed to cache previews. Click again to resume.')
-    } finally {
-      setIsCaching(false)
-      setCacheProgress(0)
+    } else {
+      toast.error('Failed to cache previews')
     }
+
+    setIsCaching(false)
+    setCacheProgress(0)
   }
 
   // Handle pattern file upload

+ 4 - 1
frontend/src/pages/PlaylistsPage.tsx

@@ -236,7 +236,10 @@ export function PlaylistsPage() {
       const newPreviews: Record<string, PreviewData> = {}
       for (const [path, previewData] of Object.entries(data)) {
         newPreviews[path] = previewData as PreviewData
-        savePreviewToCache(path, previewData as PreviewData)
+        // Only cache valid previews (with image_data and no error)
+        if (previewData && !(previewData as PreviewData).error) {
+          savePreviewToCache(path, previewData as PreviewData)
+        }
       }
       setPreviews(prev => ({ ...prev, ...newPreviews }))
     } catch (error) {

+ 1 - 1
main.py

@@ -2933,7 +2933,7 @@ async def restart_system():
             try:
                 # Use docker restart directly with container name
                 # This is simpler and doesn't require the compose file path
-                subprocess.run(["docker", "restart", "dune-weaver"], check=True)
+                subprocess.run(["docker", "restart", "dune-weaver-backend"], check=True)
                 logger.info("Docker restart command executed successfully")
             except FileNotFoundError:
                 logger.error("docker command not found")