diff --git a/frontend/src/components/Scheduler.tsx b/frontend/src/components/Scheduler.tsx index 549916e..5a2556d 100644 --- a/frontend/src/components/Scheduler.tsx +++ b/frontend/src/components/Scheduler.tsx @@ -21,6 +21,8 @@ import { notify } from "../lib/notifications"; import Tooltip from "./Tooltip"; const POLL_MS = 4000; +// While any job is running, poll faster so short scheduled runs aren't missed between ticks. +const FAST_POLL_MS = 1500; // Seconds until an ISO instant (negative = past). function secsUntil(iso: string | null): number | null { @@ -73,19 +75,25 @@ function StatusLegend() { ); } -function JobProgress({ p }: { p: NonNullable }) { +// Shown for ANY running job. With reported counts it's a determinate bar; for a job that +// runs but doesn't report progress (or hasn't yet) it falls back to a generic "working" +// label and an indeterminate sliver — so every active run is visible, not just the ones +// that report numbers. +function JobProgress({ p }: { p: SchedulerJob["progress"] }) { const { t } = useTranslation(); - const phase = p.phase ? t(`scheduler.phase.${p.phase}`, p.phase) : null; + const phase = p?.phase ? t(`scheduler.phase.${p.phase}`, p.phase) : t("scheduler.phase.working"); const pct = - p.total && p.total > 0 ? Math.min(100, Math.round((p.current / p.total) * 100)) : null; + p?.total && p.total > 0 ? Math.min(100, Math.round((p.current / p.total) * 100)) : null; return (
{phase} - {p.total != null + {p?.total != null ? `${p.current.toLocaleString()} / ${p.total.toLocaleString()}` - : p.current.toLocaleString()} + : p + ? p.current.toLocaleString() + : ""}
@@ -184,7 +192,7 @@ function JobRow({ · {job.last_result} ) : null}
- {job.running && job.progress && } + {job.running && }
{job.running ? ( @@ -298,8 +306,10 @@ function Stat({ export default function Scheduler() { const { t } = useTranslation(); const qc = useQueryClient(); + // Poll faster while any job is running so live progress is smooth, then ease back when + // idle. Derived from the freshest data by react-query itself (see useLiveQuery). const q = useLiveQuery(["scheduler"], api.schedulerStatus, { - intervalMs: POLL_MS, + intervalMs: (d) => (d?.jobs?.some((j) => j.running) ? FAST_POLL_MS : POLL_MS), }); const data = q.data; diff --git a/frontend/src/i18n/locales/de/scheduler.json b/frontend/src/i18n/locales/de/scheduler.json index 8c30032..d1a451f 100644 --- a/frontend/src/i18n/locales/de/scheduler.json +++ b/frontend/src/i18n/locales/de/scheduler.json @@ -38,7 +38,12 @@ "enrich": "Videos anreichern", "backfill_recent": "Neue Uploads nachladen", "backfill_deep": "Ganze Historie nachladen", - "probe": "Shorts klassifizieren" + "probe": "Shorts klassifizieren", + "rss_poll": "Auf neue Uploads prüfen", + "subscriptions": "Abos synchronisieren", + "autotag": "Kanäle automatisch taggen", + "playlist_sync": "Playlists synchronisieren", + "working": "Arbeitet…" }, "maintenance": { "title": "Wartung", diff --git a/frontend/src/i18n/locales/en/scheduler.json b/frontend/src/i18n/locales/en/scheduler.json index b33bcf5..f9b0073 100644 --- a/frontend/src/i18n/locales/en/scheduler.json +++ b/frontend/src/i18n/locales/en/scheduler.json @@ -38,7 +38,12 @@ "enrich": "Enriching videos", "backfill_recent": "Backfilling recent uploads", "backfill_deep": "Backfilling full history", - "probe": "Classifying Shorts" + "probe": "Classifying Shorts", + "rss_poll": "Checking for new uploads", + "subscriptions": "Syncing subscriptions", + "autotag": "Auto-tagging channels", + "playlist_sync": "Syncing playlists", + "working": "Working…" }, "maintenance": { "title": "Maintenance", diff --git a/frontend/src/i18n/locales/hu/scheduler.json b/frontend/src/i18n/locales/hu/scheduler.json index a3a1b9a..7a8bc36 100644 --- a/frontend/src/i18n/locales/hu/scheduler.json +++ b/frontend/src/i18n/locales/hu/scheduler.json @@ -38,7 +38,12 @@ "enrich": "Videók dúsítása", "backfill_recent": "Friss feltöltések behúzása", "backfill_deep": "Teljes előzmény behúzása", - "probe": "Shorts besorolás" + "probe": "Shorts besorolás", + "rss_poll": "Új feltöltések keresése", + "subscriptions": "Feliratkozások szinkronizálása", + "autotag": "Csatornák automatikus címkézése", + "playlist_sync": "Lejátszási listák szinkronizálása", + "working": "Dolgozik…" }, "maintenance": { "title": "Karbantartás", diff --git a/frontend/src/lib/useLiveQuery.ts b/frontend/src/lib/useLiveQuery.ts index 121045b..9571337 100644 --- a/frontend/src/lib/useLiveQuery.ts +++ b/frontend/src/lib/useLiveQuery.ts @@ -8,14 +8,22 @@ import { useQuery, type QueryKey } from "@tanstack/react-query"; export function useLiveQuery( key: QueryKey, queryFn: () => Promise, - opts: { intervalMs?: number; enabled?: boolean } = {} + // intervalMs may be a function of the latest data, so the poll cadence can adapt to it + // (e.g. poll faster while a job is running). react-query re-evaluates it after each fetch + // against fresh data — the right place for this, vs. a React state toggle that can lag or + // stall the live updates. + opts: { intervalMs?: number | ((data: T | undefined) => number); enabled?: boolean } = {} ) { const { intervalMs = 4000, enabled = true } = opts; return useQuery({ queryKey: key, queryFn, enabled, - refetchInterval: enabled ? intervalMs : false, + refetchInterval: !enabled + ? false + : typeof intervalMs === "function" + ? (query) => intervalMs(query.state.data as T | undefined) + : intervalMs, refetchIntervalInBackground: false, staleTime: 0, });