fix(scheduler): show progress for any running job with reliable live updates
The dashboard now renders a progress bar for any running job — determinate when counts are reported, an indeterminate "working" sliver otherwise — so a scheduled run is as visible as a manual one (progress was never manual-only; the wiring is shared, but only some jobs reported and the display gated on counts). Poll faster (1.5s) while any job runs, easing back to 4s when idle, derived from the freshest data by react-query's functional refetchInterval. The earlier React-state approach to this stalled the live updates (the row froze on the first sampled value); useLiveQuery now accepts a function of the data. Trilingual phase labels for the newly-reporting jobs.
This commit is contained in:
parent
e0971a23ec
commit
cdc6715935
5 changed files with 45 additions and 12 deletions
|
|
@ -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<SchedulerJob["progress"]> }) {
|
||||
// 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 (
|
||||
<div className="mt-1.5">
|
||||
<div className="flex items-center justify-between text-[11px] text-muted mb-0.5">
|
||||
<span className="truncate">{phase}</span>
|
||||
<span className="tabular-nums shrink-0">
|
||||
{p.total != null
|
||||
{p?.total != null
|
||||
? `${p.current.toLocaleString()} / ${p.total.toLocaleString()}`
|
||||
: p.current.toLocaleString()}
|
||||
: p
|
||||
? p.current.toLocaleString()
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-1.5 rounded-full bg-border overflow-hidden">
|
||||
|
|
@ -184,7 +192,7 @@ function JobRow({
|
|||
<span> · {job.last_result}</span>
|
||||
) : null}
|
||||
</div>
|
||||
{job.running && job.progress && <JobProgress p={job.progress} />}
|
||||
{job.running && <JobProgress p={job.progress} />}
|
||||
</div>
|
||||
<div className="shrink-0 text-right text-[11px] text-muted tabular-nums">
|
||||
{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<SchedulerStatus>(["scheduler"], api.schedulerStatus, {
|
||||
intervalMs: POLL_MS,
|
||||
intervalMs: (d) => (d?.jobs?.some((j) => j.running) ? FAST_POLL_MS : POLL_MS),
|
||||
});
|
||||
const data = q.data;
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -8,14 +8,22 @@ import { useQuery, type QueryKey } from "@tanstack/react-query";
|
|||
export function useLiveQuery<T>(
|
||||
key: QueryKey,
|
||||
queryFn: () => Promise<T>,
|
||||
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,
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue