From e6711e91a54a235d1424f41fa887316d1b262849 Mon Sep 17 00:00:00 2001 From: jlightner Date: Sat, 4 Apr 2026 06:39:17 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Added=20missing=20status=20badge,=20Lib?= =?UTF-8?q?rary=20filter/re-download=20button,=20an=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "src/frontend/src/components/StatusBadge.tsx" - "src/frontend/src/pages/Library.tsx" - "src/frontend/src/pages/System.tsx" - "src/frontend/src/api/hooks/useLibrary.ts" - "src/frontend/src/api/hooks/useSystem.ts" GSD-Task: S06/T03 --- src/frontend/src/api/hooks/useLibrary.ts | 16 ++- src/frontend/src/api/hooks/useSystem.ts | 42 ++++++++ src/frontend/src/components/StatusBadge.tsx | 1 + src/frontend/src/pages/Library.tsx | 30 +++++- src/frontend/src/pages/System.tsx | 102 +++++++++++++++++++- 5 files changed, 185 insertions(+), 6 deletions(-) diff --git a/src/frontend/src/api/hooks/useLibrary.ts b/src/frontend/src/api/hooks/useLibrary.ts index befd206..8671546 100644 --- a/src/frontend/src/api/hooks/useLibrary.ts +++ b/src/frontend/src/api/hooks/useLibrary.ts @@ -1,4 +1,4 @@ -import { useQuery } from '@tanstack/react-query'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { apiClient } from '../client'; import type { ContentItem, ContentStatus, ContentType } from '@shared/types/index'; import type { PaginatedResponse } from '@shared/types/api'; @@ -44,3 +44,17 @@ export function useLibraryContent(filters: LibraryFilters = {}) { }, }); } + +// ── Mutations ── + +/** Re-download a missing content item. Invalidates library queries on success. */ +export function useRequeueContent() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (contentId: number) => + apiClient.post(`/api/v1/content/${contentId}/requeue`), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: libraryKeys.all }); + }, + }); +} diff --git a/src/frontend/src/api/hooks/useSystem.ts b/src/frontend/src/api/hooks/useSystem.ts index eb0ede2..d0ddfe9 100644 --- a/src/frontend/src/api/hooks/useSystem.ts +++ b/src/frontend/src/api/hooks/useSystem.ts @@ -10,6 +10,7 @@ export const systemKeys = { apiKey: ['system', 'apikey'] as const, appSettings: ['system', 'appSettings'] as const, ytdlpStatus: ['system', 'ytdlpStatus'] as const, + missingScanStatus: ['system', 'missingScanStatus'] as const, }; // ── Queries ── @@ -91,3 +92,44 @@ export function useUpdateYtDlp() { }, }); } + +// ── Missing File Scan ── + +interface ScanResult { + checked: number; + missing: number; + duration: number; +} + +interface MissingScanStatusResponse { + lastRun: string; + result: ScanResult; +} + +interface MissingScanTriggerResponse { + success: boolean; + data: ScanResult; +} + +/** Fetch last missing file scan status. Does not auto-refresh. */ +export function useMissingScanStatus() { + return useQuery({ + queryKey: systemKeys.missingScanStatus, + queryFn: () => + apiClient.get<{ success: boolean; data: MissingScanStatusResponse | null }>( + '/api/v1/system/missing-scan/status', + ), + }); +} + +/** Trigger an on-demand missing file scan. Invalidates scan status on success. */ +export function useTriggerMissingScan() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: () => + apiClient.post('/api/v1/system/missing-scan'), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: systemKeys.missingScanStatus }); + }, + }); +} diff --git a/src/frontend/src/components/StatusBadge.tsx b/src/frontend/src/components/StatusBadge.tsx index 3bf2246..64b3c7e 100644 --- a/src/frontend/src/components/StatusBadge.tsx +++ b/src/frontend/src/components/StatusBadge.tsx @@ -16,6 +16,7 @@ const STATUS_STYLES: Record = { downloading: { color: 'var(--info)', backgroundColor: 'var(--info-bg)' }, failed: { color: 'var(--danger)', backgroundColor: 'var(--danger-bg)' }, queued: { color: 'var(--warning)', backgroundColor: 'var(--warning-bg)' }, + missing: { color: 'var(--warning)', backgroundColor: 'var(--warning-bg)' }, ignored: { color: 'var(--text-muted)', backgroundColor: 'var(--bg-hover)' }, // Queue statuses pending: { color: 'var(--warning)', backgroundColor: 'var(--warning-bg)' }, diff --git a/src/frontend/src/pages/Library.tsx b/src/frontend/src/pages/Library.tsx index 4c90e6b..f1ae677 100644 --- a/src/frontend/src/pages/Library.tsx +++ b/src/frontend/src/pages/Library.tsx @@ -1,6 +1,6 @@ import { useState, useCallback, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Library as LibraryIcon, RefreshCw, Film, Music } from 'lucide-react'; +import { Library as LibraryIcon, RefreshCw, Film, Music, RotateCcw } from 'lucide-react'; import { Table, type Column } from '../components/Table'; import { StatusBadge } from '../components/StatusBadge'; import { QualityLabel } from '../components/QualityLabel'; @@ -9,7 +9,7 @@ import { Pagination } from '../components/Pagination'; import { SearchBar } from '../components/SearchBar'; import { FilterBar, type FilterDefinition } from '../components/FilterBar'; import { SkeletonLibrary } from '../components/Skeleton'; -import { useLibraryContent, type LibraryFilters } from '../api/hooks/useLibrary'; +import { useLibraryContent, useRequeueContent, type LibraryFilters } from '../api/hooks/useLibrary'; import { useChannels } from '../api/hooks/useChannels'; import { formatDuration, formatFileSize } from '../utils/format'; import type { ContentItem, ContentStatus, ContentType } from '@shared/types/index'; @@ -44,6 +44,7 @@ export function Library() { // Queries const { data, isLoading, error, refetch } = useLibraryContent(filters); const { data: channels } = useChannels(); + const requeue = useRequeueContent(); // Reset to page 1 when filters change const handleSearchChange = useCallback((value: string) => { @@ -80,6 +81,7 @@ export function Library() { { value: 'downloading', label: 'Downloading' }, { value: 'downloaded', label: 'Downloaded' }, { value: 'failed', label: 'Failed' }, + { value: 'missing', label: 'Missing' }, { value: 'ignored', label: 'Ignored' }, ], }, @@ -260,8 +262,30 @@ export function Library() { ), }, + { + key: 'actions', + label: '', + width: '110px', + render: (item) => + item.status === 'missing' ? ( + + ) : null, + }, ], - [channels, navigate], + [channels, navigate, requeue], ); // Extract pagination from response diff --git a/src/frontend/src/pages/System.tsx b/src/frontend/src/pages/System.tsx index 8b1c560..ca92754 100644 --- a/src/frontend/src/pages/System.tsx +++ b/src/frontend/src/pages/System.tsx @@ -1,5 +1,5 @@ -import { RefreshCw, Server, Activity, Cpu, HardDrive } from 'lucide-react'; -import { useSystemStatus, useHealth, useYtDlpStatus, useUpdateYtDlp } from '../api/hooks/useSystem'; +import { RefreshCw, Server, Activity, Cpu, HardDrive, Search } from 'lucide-react'; +import { useSystemStatus, useHealth, useYtDlpStatus, useUpdateYtDlp, useMissingScanStatus, useTriggerMissingScan } from '../api/hooks/useSystem'; import { HealthStatus } from '../components/HealthStatus'; import { SkeletonSystem } from '../components/Skeleton'; import { formatBytes } from '../utils/format'; @@ -25,6 +25,8 @@ export function SystemPage() { const { data: status, isLoading: statusLoading, error: statusError, refetch: refetchStatus } = useSystemStatus(); const { data: ytdlpStatus, isLoading: ytdlpLoading } = useYtDlpStatus(); const updateYtDlp = useUpdateYtDlp(); + const { data: missingScanData } = useMissingScanStatus(); + const triggerMissingScan = useTriggerMissingScan(); const isLoading = healthLoading || statusLoading; @@ -92,6 +94,102 @@ export function SystemPage() { ) : null} + {/* ── Missing File Scan section ── */} +
+
+

+ + Missing File Scan +

+ +
+ +
+ {triggerMissingScan.data && ( +
0 ? 'var(--warning-bg)' : 'var(--success-bg)', + color: triggerMissingScan.data.data.missing > 0 ? 'var(--warning)' : 'var(--success)', + fontSize: 'var(--font-size-sm)', + fontWeight: 500, + }} + > + Scan complete: checked {triggerMissingScan.data.data.checked} files, found {triggerMissingScan.data.data.missing} missing + {' '}({(triggerMissingScan.data.data.duration / 1000).toFixed(1)}s) +
+ )} + + {triggerMissingScan.error && ( +
+ Scan failed: {triggerMissingScan.error instanceof Error ? triggerMissingScan.error.message : 'Unknown error'} +
+ )} + + {missingScanData?.data ? ( + + + } + label="Last Scan" + value={new Date(missingScanData.data.lastRun).toLocaleString()} + /> + } + label="Files Checked" + value={String(missingScanData.data.result.checked)} + /> + 0 ? 'var(--warning)' : 'var(--success)' }} />} + label="Missing Files" + value={String(missingScanData.data.result.missing)} + /> + +
+ ) : ( +

+ No scan has been run yet. Click "Scan Now" to check for missing files. +

+ )} +
+
+ {/* ── System Status section ── */}