diff --git a/src/frontend/src/api/hooks/useChannels.ts b/src/frontend/src/api/hooks/useChannels.ts index 4d8582c..a3560dc 100644 --- a/src/frontend/src/api/hooks/useChannels.ts +++ b/src/frontend/src/api/hooks/useChannels.ts @@ -86,9 +86,9 @@ export function useDeleteChannel() { export interface ScanChannelResult { channelId: number; channelName: string; - newItems: number; - totalFetched: number; - status: 'success' | 'error' | 'rate_limited' | 'already_running'; + newItems?: number; + totalFetched?: number; + status: 'started' | 'success' | 'error' | 'rate_limited' | 'already_running'; } export interface ScanAllResult { @@ -98,18 +98,22 @@ export interface ScanAllResult { // ── Scan Mutations ── -/** Trigger a manual scan for a single channel. */ +/** + * Trigger a manual scan for a single channel. + * Returns immediately with status 'started' — progress is streamed via WebSocket. + */ export function useScanChannel(id: number) { - const queryClient = useQueryClient(); - return useMutation({ mutationFn: () => apiClient.post(`/api/v1/channel/${id}/scan`), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: channelKeys.all }); - queryClient.invalidateQueries({ queryKey: channelKeys.detail(id) }); - queryClient.invalidateQueries({ queryKey: contentKeys.byChannel(id) }); - }, + }); +} + +/** Cancel an in-progress scan for a channel. */ +export function useCancelScan(id: number) { + return useMutation({ + mutationFn: () => + apiClient.post<{ channelId: number; cancelled: boolean }>(`/api/v1/channel/${id}/scan-cancel`), }); } diff --git a/src/frontend/src/pages/ChannelDetail.tsx b/src/frontend/src/pages/ChannelDetail.tsx index 3a47737..3e33ee1 100644 --- a/src/frontend/src/pages/ChannelDetail.tsx +++ b/src/frontend/src/pages/ChannelDetail.tsx @@ -19,10 +19,11 @@ import { Music, RefreshCw, Save, + Square, Trash2, Users, } from 'lucide-react'; -import { useChannel, useUpdateChannel, useDeleteChannel, useScanChannel, useSetMonitoringMode } from '../api/hooks/useChannels'; +import { useChannel, useUpdateChannel, useDeleteChannel, useScanChannel, useCancelScan, useSetMonitoringMode } from '../api/hooks/useChannels'; import { useChannelContentPaginated, useDownloadContent, useToggleMonitored, useBulkMonitored, useCollectMonitored, type ChannelContentFilters } from '../api/hooks/useContent'; import { useChannelPlaylists, useRefreshPlaylists } from '../api/hooks/usePlaylists'; import { useFormatProfiles } from '../api/hooks/useFormatProfiles'; @@ -166,6 +167,7 @@ export function ChannelDetail() { const deleteChannel = useDeleteChannel(); const downloadContent = useDownloadContent(); const scanChannel = useScanChannel(channelId); + const cancelScan = useCancelScan(channelId); const collectMonitored = useCollectMonitored(channelId); const setMonitoringMode = useSetMonitoringMode(channelId); const toggleMonitored = useToggleMonitored(channelId); @@ -173,7 +175,7 @@ export function ChannelDetail() { const bulkMonitored = useBulkMonitored(channelId); // ── Scan state (WebSocket-driven) ── - const { scanning: scanInProgress, newItemCount: _scanNewItemCount } = useScanProgress(channelId); + const { scanning: scanInProgress, newItemCount: scanNewItemCount } = useScanProgress(channelId); // ── Local state ── const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); @@ -256,16 +258,8 @@ export function ChannelDetail() { onSuccess: (result) => { if (result.status === 'already_running') { toast('Scan already in progress', 'info'); - } else if (result.status === 'rate_limited') { - toast('Rate limited — try again later', 'info'); - } else if (result.status === 'error') { - toast('Scan failed — check server logs', 'error'); - } else { - // Scan completed synchronously (fast — small channel or cached) - const msg = result.newItems > 0 - ? `Found ${result.newItems} new item${result.newItems === 1 ? '' : 's'}` - : 'No new content'; - toast(msg, 'success'); + } else if (result.status === 'started') { + toast('Scan started — items will appear as they\'re found', 'success'); } }, onError: (err) => { @@ -274,6 +268,21 @@ export function ChannelDetail() { }); }, [scanChannel, toast]); + const handleCancelScan = useCallback(() => { + cancelScan.mutate(undefined, { + onSuccess: (result) => { + if (result.cancelled) { + toast('Scan cancelled', 'info'); + } else { + toast('No scan in progress', 'info'); + } + }, + onError: (err) => { + toast(err instanceof Error ? err.message : 'Failed to cancel scan', 'error'); + }, + }); + }, [cancelScan, toast]); + const handleCollect = useCallback(() => { collectMonitored.mutate(undefined, { onSuccess: (result) => { @@ -973,18 +982,20 @@ export function ChannelDetail() {