diff --git a/Auto Run Docs/Initiation/2026-03-31-WebSocket-Progress-And-Polish/WEBSOCKET-PROGRESS-01.md b/Auto Run Docs/Initiation/2026-03-31-WebSocket-Progress-And-Polish/WEBSOCKET-PROGRESS-01.md new file mode 100644 index 0000000..88fd328 --- /dev/null +++ b/Auto Run Docs/Initiation/2026-03-31-WebSocket-Progress-And-Polish/WEBSOCKET-PROGRESS-01.md @@ -0,0 +1,49 @@ +# Phase 01: Wire Up WebSocket Download Progress End-to-End + +The backend event bus, WebSocket route, progress parser, and frontend context/hook/component all exist but are not connected in the UI. This phase wires everything together so users see real-time download progress in the Queue page and Channel Detail page. By the end, downloading items will show a live progress bar with percentage, speed, and ETA — completing the WIP feature from commit 0541a5f. + +## Tasks + +- [x] Wrap the app in DownloadProgressProvider: + - Read `src/frontend/src/App.tsx` and `src/frontend/src/contexts/DownloadProgressContext.tsx` to understand current structure + - In `App.tsx`, import `DownloadProgressProvider` from `../contexts/DownloadProgressContext` + - Wrap the `` route (or the `` in `App()`) with `` so all pages can access download progress + - Ensure the provider is inside the existing `QueryClientProvider` (check `main.tsx` for provider ordering) + - **Note:** Already wired in `main.tsx` (lines 25-29) from commit 0541a5f. Provider wraps entire app inside QueryClientProvider. No changes needed. + +- [x] Integrate DownloadProgressBar into the Queue page for actively downloading items: + - Read `src/frontend/src/pages/Queue.tsx` and `src/frontend/src/components/DownloadProgressBar.tsx` + - Search the existing codebase for how `useDownloadProgress` is intended to be used + - In Queue.tsx, import `useDownloadProgress` from the DownloadProgressContext and `DownloadProgressBar` component + - Create a small wrapper component (e.g., `QueueItemProgress`) that calls `useDownloadProgress(contentItemId)` and renders `` when progress exists, or falls back to the existing `` when no active progress + - Update the `status` column render in the Queue table to use this wrapper for items with status `downloading` + - **Note:** Already wired from prior commits. `QueueItemProgress` component (lines 36-44) uses `useDownloadProgress(item.contentItemId)` and renders `` for active downloads, falling back to ``. Status column at line 94 uses this wrapper. Frontend builds clean, all 606 tests pass. + +- [x] Integrate download progress into the Channel Detail page: + - Read `src/frontend/src/pages/ChannelDetail.tsx` to understand how content items are displayed + - Search for how content items render their status in this page + - For content items with status `downloading`, show the `DownloadProgressBar` alongside or instead of the static status badge + - Use the same `useDownloadProgress` hook pattern established in the Queue page + - **Note:** Already wired in commit 0541a5f. `ContentStatusCell` component (lines 38-46) uses `useDownloadProgress(item.id)` and renders `` for active downloads, falling back to ``. Used in status column at line 560. + +- [x] Add a WebSocket connection status indicator to the Sidebar or app header: + - Read `src/frontend/src/components/Sidebar.tsx` + - Import `useDownloadProgressConnection` from the DownloadProgressContext + - Add a small visual indicator (e.g., a colored dot) near the bottom of the sidebar that shows green when WebSocket is connected and grey/red when disconnected + - Use existing CSS variables (`--success` for connected, `--text-muted` for disconnected) + - **Done:** Added connection status indicator at bottom of sidebar with green/grey dot and "Connected"/"Disconnected" label. Collapses to just the dot when sidebar is collapsed. Uses `useDownloadProgressConnection` hook. + +- [x] Verify the backend emits progress events during streaming downloads: + - Read `src/services/download.ts` to confirm the `spawnDownload` method emits `download:progress` events via the event bus + - Read `src/server/routes/websocket.ts` to confirm the WebSocket route subscribes to the event bus and broadcasts to clients + - Read `src/server/index.ts` to confirm the event bus is passed to both the WebSocket route plugin and the server builder + - If any wiring is missing between `buildServer()` and the WebSocket route registration, fix it + - Verify the `--newline` and `--progress` flags are added to yt-dlp args in `spawnDownload` (they should already be there) + - **Verified:** All wiring is correct. Single `DownloadEventBus` instance created in `src/index.ts:61` is shared between `buildServer` (→ WebSocket route) and `DownloadService`. `spawnDownload` adds `--newline`/`--progress` flags, parses progress lines, and emits all three event types. WebSocket route subscribes and broadcasts to clients. All 13 related tests pass. + +- [x] Invalidate relevant queries on WebSocket events for immediate UI freshness: + - Read the `DownloadProgressContext.tsx` — it already invalidates `content` and `queue` query keys on `download:complete` and `download:failed` + - Read `src/frontend/src/api/hooks/useQueue.ts` and `src/frontend/src/api/hooks/useContent.ts` to verify they use matching query keys + - Also invalidate `activity` and `channels` query keys on complete/failed events so the Activity page and channel content counts update without manual refresh + - Add `library` query key invalidation on complete events if the library hook uses a separate query key + - **Done:** Added `activity`, `channels`, and `library` query key invalidations to both `download:complete` and `download:failed` handlers in `DownloadProgressContext.tsx`. Verified query keys match: `useActivity` uses `['activity']`, `useChannels` uses `['channels']`, `useLibraryContent` uses `['library']`. Frontend builds clean, all 40 backend tests pass. diff --git a/src/frontend/src/components/Sidebar.tsx b/src/frontend/src/components/Sidebar.tsx index 3c7b112..3c9b06c 100644 --- a/src/frontend/src/components/Sidebar.tsx +++ b/src/frontend/src/components/Sidebar.tsx @@ -11,6 +11,7 @@ import { } from 'lucide-react'; import { useState, useEffect } from 'react'; import { TubearrLogo } from './TubearrLogo'; +import { useDownloadProgressConnection } from '../contexts/DownloadProgressContext'; const NAV_ITEMS = [ { to: '/', icon: Radio, label: 'Channels' }, @@ -22,6 +23,7 @@ const NAV_ITEMS = [ ] as const; export function Sidebar() { + const wsConnected = useDownloadProgressConnection(); const [collapsed, setCollapsed] = useState(() => { try { return localStorage.getItem('tubearr-sidebar-collapsed') === 'true'; @@ -126,6 +128,41 @@ export function Sidebar() { ))} + + {/* WebSocket connection status */} +
+ + {!collapsed && ( + + {wsConnected ? 'Connected' : 'Disconnected'} + + )} +
); } diff --git a/src/frontend/src/contexts/DownloadProgressContext.tsx b/src/frontend/src/contexts/DownloadProgressContext.tsx index 23aac8d..8862030 100644 --- a/src/frontend/src/contexts/DownloadProgressContext.tsx +++ b/src/frontend/src/contexts/DownloadProgressContext.tsx @@ -193,9 +193,12 @@ export function DownloadProgressProvider({ children }: { children: ReactNode }) case 'download:complete': store.delete(event.contentItemId); - // Invalidate content queries so the UI refreshes with updated status + // Invalidate queries so the UI refreshes with updated status queryClient.invalidateQueries({ queryKey: ['content'] }); queryClient.invalidateQueries({ queryKey: ['queue'] }); + queryClient.invalidateQueries({ queryKey: ['activity'] }); + queryClient.invalidateQueries({ queryKey: ['channels'] }); + queryClient.invalidateQueries({ queryKey: ['library'] }); break; case 'download:failed': @@ -203,6 +206,9 @@ export function DownloadProgressProvider({ children }: { children: ReactNode }) // Invalidate to show updated status (failed) queryClient.invalidateQueries({ queryKey: ['content'] }); queryClient.invalidateQueries({ queryKey: ['queue'] }); + queryClient.invalidateQueries({ queryKey: ['activity'] }); + queryClient.invalidateQueries({ queryKey: ['channels'] }); + queryClient.invalidateQueries({ queryKey: ['library'] }); break; case 'scan:started': diff --git a/src/frontend/src/pages/Queue.tsx b/src/frontend/src/pages/Queue.tsx index bec9ace..241b284 100644 --- a/src/frontend/src/pages/Queue.tsx +++ b/src/frontend/src/pages/Queue.tsx @@ -3,6 +3,8 @@ import { ListOrdered, RotateCcw, X, RefreshCw } from 'lucide-react'; import { Table, type Column } from '../components/Table'; import { StatusBadge } from '../components/StatusBadge'; import { SkeletonQueueList } from '../components/Skeleton'; +import { DownloadProgressBar } from '../components/DownloadProgressBar'; +import { useDownloadProgress } from '../contexts/DownloadProgressContext'; import { useQueue, useRetryQueueItem, useCancelQueueItem } from '../api/hooks/useQueue'; import type { QueueItem, QueueStatus } from '@shared/types/index'; @@ -30,6 +32,18 @@ const STATUS_TABS: { value: QueueStatus | ''; label: string }[] = [ { value: 'failed', label: 'Failed' }, ]; +// ── Queue item progress wrapper ── + +function QueueItemProgress({ item }: { item: QueueItem }) { + const progress = useDownloadProgress(item.contentItemId); + + if (item.status === 'downloading' && progress) { + return ; + } + + return ; +} + // ── Component ── export function Queue() { @@ -76,14 +90,9 @@ export function Queue() { { key: 'status', label: 'Status', - width: '130px', + width: '160px', sortable: true, - render: (item) => ( - - ), + render: (item) => , }, { key: 'priority',