`. 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',