Show discovery progress in the top-bar indicator alongside downloads/tasks
The DownloadIndicator in Layout.jsx now queries discovery-status and shows discovery progress the same way it shows active downloads and channel tasks: - Spinning icon appears in the navbar when discovery is running - Hover popover shows "Discovering channels" with X/Y count and a progress bar - Polls every 10 s while running, 60 s when idle - Primary label priority: download % > task phase > discovery X/Y Discovery page header simplified: progress bar and verbose status removed since the top-bar indicator now handles it. "Find more" button still disables while running. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { useRef, useEffect } from "react";
|
||||
import { useAuth } from "../hooks/useAuth";
|
||||
import SearchBar from "./SearchBar";
|
||||
import { getDownloads, getChannels, getActiveTasks, getMe } from "../api";
|
||||
import { getDownloads, getChannels, getActiveTasks, getDiscoveryStatus, getMe } from "../api";
|
||||
|
||||
function BottomNav({ newCount }) {
|
||||
const tabs = [
|
||||
@@ -91,20 +91,29 @@ function DownloadIndicator() {
|
||||
refetchInterval: (query) => (query.state.data?.length > 0 ? 2000 : 10_000),
|
||||
});
|
||||
|
||||
const { data: discStatus } = useQuery({
|
||||
queryKey: ["discovery-status"],
|
||||
queryFn: () => getDiscoveryStatus().then((r) => r.data),
|
||||
refetchInterval: (query) => (query.state.data?.progress?.running ? 10_000 : 60_000),
|
||||
staleTime: 10_000,
|
||||
});
|
||||
|
||||
const activeDownloads = (downloads ?? []).filter(
|
||||
(d) => d.status === "pending" || d.status === "downloading"
|
||||
);
|
||||
const discRunning = discStatus?.progress?.running;
|
||||
const discProgress = discStatus?.progress;
|
||||
|
||||
if (!activeDownloads.length && !tasks.length) return null;
|
||||
if (!activeDownloads.length && !tasks.length && !discRunning) return null;
|
||||
|
||||
const totalActive = activeDownloads.length + tasks.length;
|
||||
const totalActive = activeDownloads.length + tasks.length + (discRunning ? 1 : 0);
|
||||
|
||||
// Show download progress if there are active downloads, otherwise show task phase
|
||||
// Primary label: download % > task phase > discovery
|
||||
let label;
|
||||
if (activeDownloads.length) {
|
||||
const pct = activeDownloads[0].progress_percent ?? 0;
|
||||
label = <span className="font-mono tabular-nums text-[11px]">{pct.toFixed(0)}%</span>;
|
||||
} else {
|
||||
} else if (tasks.length) {
|
||||
const task = tasks[0];
|
||||
const pct = task.total > 0 ? Math.round((task.done / task.total) * 100) : null;
|
||||
label = (
|
||||
@@ -112,12 +121,18 @@ function DownloadIndicator() {
|
||||
{pct !== null ? `${pct}%` : task.phase || "…"}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
label = (
|
||||
<span className="text-[11px] hidden sm:inline">
|
||||
{discProgress ? `${discProgress.done}/${discProgress.total}` : "…"}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative group shrink-0">
|
||||
<Link
|
||||
to="/downloads"
|
||||
to="/discovery"
|
||||
className="flex items-center gap-1.5 px-2 py-1 rounded-lg bg-zinc-800 hover:bg-zinc-700 transition-colors text-xs text-zinc-300"
|
||||
>
|
||||
<svg className="w-3 h-3 animate-spin text-zinc-400 shrink-0" fill="none" viewBox="0 0 24 24">
|
||||
@@ -163,6 +178,19 @@ function DownloadIndicator() {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{discRunning && discProgress && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between gap-2 mb-1">
|
||||
<p className="text-xs text-zinc-200">Discovering channels</p>
|
||||
<span className="text-[10px] text-zinc-400 tabular-nums shrink-0">{discProgress.done}/{discProgress.total}</span>
|
||||
</div>
|
||||
<p className="text-[10px] text-zinc-500 mb-1">Finding channels… spaced over ~20 min</p>
|
||||
<div className="h-0.5 bg-zinc-800 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-accent rounded-full transition-all duration-300"
|
||||
style={{ width: `${Math.round((discProgress.done / discProgress.total) * 100)}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -218,7 +218,7 @@ export default function DiscoveryPage() {
|
||||
queryKey: ["discovery-status"],
|
||||
queryFn: () => getDiscoveryStatus().then(r => r.data),
|
||||
staleTime: 10_000,
|
||||
refetchInterval: (query) => query.state.data?.progress?.running ? 10_000 : 60_000,
|
||||
refetchInterval: (query) => (query.state.data?.progress?.running ? 10_000 : 60_000),
|
||||
});
|
||||
|
||||
const refreshMut = useMutation({
|
||||
@@ -248,13 +248,11 @@ export default function DiscoveryPage() {
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="min-w-0">
|
||||
<h1 className="font-display font-bold text-2xl text-zinc-100">Discover</h1>
|
||||
{discStatus && (
|
||||
<p className="text-xs text-zinc-500 mt-0.5">
|
||||
{discStatus.progress?.running
|
||||
? `Finding channels… ${discStatus.progress.done} / ${discStatus.progress.total}`
|
||||
: discStatus.pending_count > 0
|
||||
{discStatus.pending_count > 0
|
||||
? `${discStatus.pending_count} channel${discStatus.pending_count !== 1 ? "s" : ""} queued`
|
||||
: "Queue empty"}
|
||||
{discStatus.last_run
|
||||
@@ -262,38 +260,25 @@ export default function DiscoveryPage() {
|
||||
: " · never refreshed"}
|
||||
</p>
|
||||
)}
|
||||
{discStatus?.progress?.running && (
|
||||
<div className="mt-1.5 h-1 w-48 rounded-full bg-zinc-800 overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-accent rounded-full transition-all duration-500"
|
||||
style={{ width: `${Math.round((discStatus.progress.done / discStatus.progress.total) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => refreshMut.mutate()}
|
||||
disabled={refreshMut.isPending || discStatus?.progress?.running}
|
||||
className="shrink-0 flex items-center gap-2 text-sm font-medium px-4 py-2 bg-zinc-800 text-zinc-300 rounded-lg hover:bg-zinc-700 transition-colors disabled:opacity-60"
|
||||
>
|
||||
{discStatus?.progress?.running ? (
|
||||
{refreshMut.isPending && (
|
||||
<svg className="w-3.5 h-3.5 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
|
||||
</svg>
|
||||
) : refreshMut.isPending ? (
|
||||
<svg className="w-3.5 h-3.5 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
|
||||
</svg>
|
||||
) : null}
|
||||
)}
|
||||
{discStatus?.progress?.running ? "Running…" : refreshMut.isSuccess ? "Queued ✓" : refreshMut.isPending ? "Queueing…" : "Find more"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{(refreshMut.isSuccess || discStatus?.progress?.running) && (
|
||||
{refreshMut.isSuccess && !discStatus?.progress?.running && (
|
||||
<div className="px-4 py-3 rounded-xl bg-zinc-800/60 border border-zinc-700/50 text-sm text-zinc-400">
|
||||
Discovery is running in the background — searches and channel fetches are spaced out over ~20 minutes to avoid hitting limits. New channels appear as each batch completes. Runs automatically every day.
|
||||
Discovery is running — progress shows in the top bar. Searches are spaced out over ~20 minutes. Runs automatically every day.
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user