feat: settings panel + thumbnail pipeline fixes

- photos.py: stop crashing in FileResponse when a thumb hasn't been
  generated; return a clean 404 with Retry-After so the frontend can
  back off.
- thumbs.py: fix process_video_thumbnail (overwrite_output, robust
  duration probe across stream/format, eager frame load + temp cleanup)
  so videos stop ending up as the gray placeholder.
- library.py: new /maintenance/* endpoints — thumbnail-stats,
  regenerate-thumbnails (with media_type / only_failed filters), and a
  manual data-integrity cleanup trigger.
- Frontend Settings panel (gear in TopBar) surfacing those endpoints
  plus a re-scan button and live thumbnail status counts.
- PhotoThumbnail: stretch the auto-retry schedule for slow RAW jobs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-09 09:24:08 +02:00
parent 6fa00f4b37
commit 9ed577f40c
8 changed files with 719 additions and 33 deletions

View File

@@ -207,6 +207,29 @@ export const photos = {
}
// Library API
export type MediaType = 'photo' | 'raw' | 'heic' | 'video'
export interface ThumbnailStats {
total: number
pending: number
processing: number
completed: number
failed: number
by_media_type: Record<string, number>
}
export interface RegenerateResult {
status: string
matched: number
queued: number
cleared_dirs: number
file_errors: number
filters: {
media_types: MediaType[] | null
only_failed: boolean
}
}
export const library = {
scan: async () => {
const response = await api.post('/library/scan')
@@ -222,6 +245,33 @@ export const library = {
const response = await api.get('/library/stats')
return response.data
},
/** Maintenance / admin actions surfaced via the Settings panel. */
maintenance: {
thumbnailStats: async (): Promise<ThumbnailStats> => {
const response = await api.get('/library/maintenance/thumbnail-stats')
return response.data
},
/** Reset on-disk thumbs and re-queue Celery generation. With no
* filters, every photo in the library is re-queued. */
regenerateThumbnails: async (
body: { media_types?: MediaType[]; only_failed?: boolean } = {}
): Promise<RegenerateResult> => {
const response = await api.post(
'/library/maintenance/regenerate-thumbnails',
body
)
return response.data
},
/** Re-run the source-roots / folders / photos integrity cleanup that
* normally runs on backend startup. */
cleanup: async (): Promise<{ status: string; message?: string }> => {
const response = await api.post('/library/maintenance/cleanup')
return response.data
},
},
}
export interface LibraryStats {