Photos grid was fetching per_page=500 on the very first request, which serialized hundreds of thumbnail requests behind a single sort+payload. Split into PER_PAGE_INITIAL=100 (one viewport, fast paint) and PER_PAGE_BACKGROUND=500 (subsequent prefetch pages, fewer round-trips). Idle polling for scan-status and worker-status was set to 10s / 15s respectively. With nothing queued the typical session was firing 4–6 status requests every minute through the single uvicorn event loop on top of everything else. Bumped both to 30s. While actively scanning / processing the 2s / 3s cadence is unchanged — that's where the user actually wants live updates. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
102 lines
3.5 KiB
TypeScript
102 lines
3.5 KiB
TypeScript
import { useEffect, useRef } from 'react'
|
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
|
import { library, WorkerStatus } from '../services/api'
|
|
|
|
interface ScanStatus {
|
|
is_scanning: boolean
|
|
current_folder?: string
|
|
processed_files: number
|
|
total_files: number
|
|
errors: string[]
|
|
}
|
|
|
|
type Phase = 'idle' | 'scanning' | 'processing' | 'done'
|
|
|
|
/**
|
|
* Headless background-activity orchestrator. Polls scan + worker status
|
|
* and invalidates affected query caches when a scan/processing pass
|
|
* completes. The visible status indicator now lives inline in the
|
|
* LeftSidebar (small spinner next to the FOLDERS header / specific
|
|
* folder rows) — see useScanActivity.
|
|
*/
|
|
export function ScanProgress() {
|
|
const queryClient = useQueryClient()
|
|
const wasScanningRef = useRef(false)
|
|
const wasProcessingRef = useRef(false)
|
|
|
|
const { data: scanStatus } = useQuery<ScanStatus>({
|
|
queryKey: ['scan-status'],
|
|
queryFn: () => library.scanStatus(),
|
|
refetchInterval: (query) =>
|
|
query.state.data?.is_scanning ? 2000 : 30000,
|
|
enabled: true,
|
|
})
|
|
|
|
const isScanning = scanStatus?.is_scanning ?? false
|
|
|
|
// Poll worker status to track vision queue activity.
|
|
// Fast polling (3s) while processing, slow (30s) otherwise.
|
|
const { data: workerStatus } = useQuery<WorkerStatus>({
|
|
queryKey: ['worker-status-progress'],
|
|
queryFn: () => library.maintenance.workerStatus(),
|
|
refetchInterval: (query) => {
|
|
const q = totalQueued(query.state.data)
|
|
return q > 0 ? 3000 : 30000
|
|
},
|
|
enabled: true,
|
|
})
|
|
|
|
const totalActive = totalQueued(workerStatus)
|
|
|
|
const phase: Phase = isScanning
|
|
? 'scanning'
|
|
: totalActive > 0
|
|
? 'processing'
|
|
: 'idle'
|
|
|
|
useEffect(() => {
|
|
if (phase === 'scanning') {
|
|
wasScanningRef.current = true
|
|
wasProcessingRef.current = false
|
|
} else if (phase === 'processing') {
|
|
wasProcessingRef.current = true
|
|
if (wasScanningRef.current) {
|
|
wasScanningRef.current = false
|
|
queryClient.invalidateQueries({ queryKey: ['photos'] })
|
|
queryClient.invalidateQueries({ queryKey: ['folders'] })
|
|
queryClient.invalidateQueries({ queryKey: ['folders', 'tree'] })
|
|
queryClient.invalidateQueries({ queryKey: ['heaps'] })
|
|
queryClient.invalidateQueries({ queryKey: ['tags'] })
|
|
queryClient.invalidateQueries({ queryKey: ['library', 'stats'] })
|
|
}
|
|
} else if (phase === 'idle') {
|
|
if (wasScanningRef.current) {
|
|
wasScanningRef.current = false
|
|
queryClient.invalidateQueries({ queryKey: ['photos'] })
|
|
queryClient.invalidateQueries({ queryKey: ['folders'] })
|
|
queryClient.invalidateQueries({ queryKey: ['folders', 'tree'] })
|
|
queryClient.invalidateQueries({ queryKey: ['heaps'] })
|
|
queryClient.invalidateQueries({ queryKey: ['tags'] })
|
|
queryClient.invalidateQueries({ queryKey: ['library', 'stats'] })
|
|
}
|
|
if (wasProcessingRef.current) {
|
|
wasProcessingRef.current = false
|
|
queryClient.invalidateQueries({ queryKey: ['tags'] })
|
|
queryClient.invalidateQueries({ queryKey: ['photos'] })
|
|
queryClient.invalidateQueries({ queryKey: ['library', 'stats'] })
|
|
}
|
|
}
|
|
}, [phase, queryClient])
|
|
|
|
return null
|
|
}
|
|
|
|
function totalQueued(ws: WorkerStatus | undefined): number {
|
|
if (!ws) return 0
|
|
const queued = Object.values(ws.queues ?? {}).reduce((a, b) => a + b, 0)
|
|
const active = ws.workers?.reduce(
|
|
(sum, w) => sum + (w.active ?? 0) + (w.reserved ?? 0), 0
|
|
) ?? 0
|
|
return queued + active
|
|
}
|