From cbe3e7c9b5b9f1034ff44748b0f78ad00b86604e Mon Sep 17 00:00:00 2001 From: npeter83 Date: Fri, 19 Jun 2026 02:57:45 +0200 Subject: [PATCH] fix(header): show sync activity only while a job is actually running MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The header's sync indicator spun "fetching history" whenever any channel still lacked full history — i.e. perpetually, even when the scheduler was idle between its periodic runs. It now spins only while a channel-sync job (backfill or RSS poll) is actually running; otherwise pending deep-history work shows as the calm, static "N without full history" link, and recent work queued for the next run shows a static "N queued". This makes the header coherent with the Scheduler dashboard's live state. Backend exposes running_job_ids() from the scheduler activity and a derived sync_active flag on /api/sync/my-status. --- backend/app/routes/sync.py | 4 ++ backend/app/scheduler.py | 7 ++++ frontend/src/components/SyncStatus.tsx | 48 +++++++++++++++--------- frontend/src/i18n/locales/de/header.json | 2 + frontend/src/i18n/locales/en/header.json | 2 + frontend/src/i18n/locales/hu/header.json | 2 + frontend/src/lib/api.ts | 1 + 7 files changed, 48 insertions(+), 18 deletions(-) diff --git a/backend/app/routes/sync.py b/backend/app/routes/sync.py index 8791aed..02ffa91 100644 --- a/backend/app/routes/sync.py +++ b/backend/app/routes/sync.py @@ -13,6 +13,7 @@ from app.sync.runner import ( run_rss_poll, ) from app.sync.subscriptions import import_subscriptions +from app.scheduler import running_job_ids router = APIRouter(prefix="/api/sync", tags=["sync"]) @@ -170,6 +171,9 @@ def my_status( "quota_used_today": quota.units_used_today(db), "quota_remaining_today": quota.remaining_today(db), "paused": state.is_sync_paused(db), + # True only while a job that advances this view (channel sync) is actually running, + # so the header shows live activity rather than a perpetual spinner over pending work. + "sync_active": bool({"backfill", "rss_poll"} & running_job_ids()), } diff --git a/backend/app/scheduler.py b/backend/app/scheduler.py index 7541b0d..abcb70f 100644 --- a/backend/app/scheduler.py +++ b/backend/app/scheduler.py @@ -272,6 +272,13 @@ def _is_running(job_id: str) -> bool: return bool(_activity.get(job_id, {}).get("running")) +def running_job_ids() -> set[str]: + """Ids of jobs executing right now (any trigger). Lets non-admin surfaces — e.g. the + header's sync indicator — reflect real activity instead of just pending-work counts.""" + with _activity_lock: + return {jid for jid, a in _activity.items() if a.get("running")} + + def trigger_job(job_id: str, actor_id: int | None = None) -> str: """Run a job immediately in a background thread, independent of its interval schedule. The wrapper handles its own DB session, activity tracking and pause-skip, so the live diff --git a/frontend/src/components/SyncStatus.tsx b/frontend/src/components/SyncStatus.tsx index 0175365..56bbb19 100644 --- a/frontend/src/components/SyncStatus.tsx +++ b/frontend/src/components/SyncStatus.tsx @@ -1,6 +1,6 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useTranslation } from "react-i18next"; -import { Database, History, Loader2, Pause, Play } from "lucide-react"; +import { Clock, Database, History, Loader2, Pause, Play } from "lucide-react"; import { api } from "../lib/api"; import { formatViews } from "../lib/format"; import Tooltip from "./Tooltip"; @@ -38,6 +38,10 @@ export default function SyncStatus({ const syncing = data.channels_recent_pending; const notFull = data.channels_deep_pending; // your channels without full history yet + // Spin only while a sync job is actually running; otherwise pending work shows as a calm, + // static state (or, for deep history, just the "N without full history" link below). + const active = data.sync_active; + const showMain = data.paused || active || syncing > 0 || notFull === 0; return (
@@ -54,23 +58,31 @@ export default function SyncStatus({ - · - {data.paused ? ( - {t("header.sync.paused")} - ) : syncing > 0 ? ( - - - {t("header.sync.syncing", { count: syncing })} - - ) : notFull > 0 ? ( - // Recent uploads are in, but the scheduler is still backfilling full history — - // so "all synced" would be misleading. Reflect the deep work as active. - - - {t("header.sync.backfillingHistory")} - - ) : ( - {t("header.sync.allSynced")} + {showMain && ( + <> + · + {data.paused ? ( + {t("header.sync.paused")} + ) : active ? ( + // A sync job is running right now — spin and name what it's doing. + + + {syncing > 0 + ? t("header.sync.syncing", { count: syncing }) + : notFull > 0 + ? t("header.sync.backfillingHistory") + : t("header.sync.working")} + + ) : syncing > 0 ? ( + // Recent uploads queued for the next run, but nothing running now — static. + + + {t("header.sync.recentQueued", { count: syncing })} + + ) : ( + {t("header.sync.allSynced")} + )} + )} {notFull > 0 && ( <> diff --git a/frontend/src/i18n/locales/de/header.json b/frontend/src/i18n/locales/de/header.json index 6c24304..2fcc887 100644 --- a/frontend/src/i18n/locales/de/header.json +++ b/frontend/src/i18n/locales/de/header.json @@ -31,6 +31,8 @@ "paused": "pausiert", "syncing": "{{count}} werden synchronisiert", "backfillingHistory": "Verlauf wird geladen", + "working": "Synchronisierung…", + "recentQueued": "{{count}} in Warteschlange", "allSynced": "alles synchronisiert", "withoutFullHistory": "{{count}} ohne vollständigen Verlauf", "fullHistoryTooltip": "Einige deiner Kanäle haben nur ihre neuesten Uploads. Klicke, um die Kanalverwaltung (auf diese gefiltert) zu öffnen und ihren vollständigen Katalog anzufordern.", diff --git a/frontend/src/i18n/locales/en/header.json b/frontend/src/i18n/locales/en/header.json index 794deea..0c027fe 100644 --- a/frontend/src/i18n/locales/en/header.json +++ b/frontend/src/i18n/locales/en/header.json @@ -31,6 +31,8 @@ "paused": "paused", "syncing": "{{count}} syncing", "backfillingHistory": "fetching history", + "working": "syncing…", + "recentQueued": "{{count}} queued", "allSynced": "all synced", "withoutFullHistory": "{{count}} without full history", "fullHistoryTooltip": "Some of your channels only have their recent uploads. Click to open the channel manager (filtered to these) and request their full back-catalog.", diff --git a/frontend/src/i18n/locales/hu/header.json b/frontend/src/i18n/locales/hu/header.json index 0e8b17f..f2d6531 100644 --- a/frontend/src/i18n/locales/hu/header.json +++ b/frontend/src/i18n/locales/hu/header.json @@ -31,6 +31,8 @@ "paused": "szüneteltetve", "syncing": "{{count}} szinkronizálás alatt", "backfillingHistory": "előzmény letöltése", + "working": "szinkronizálás…", + "recentQueued": "{{count}} sorban", "allSynced": "minden szinkronban", "withoutFullHistory": "{{count}} teljes előzmény nélkül", "fullHistoryTooltip": "Néhány csatornádnak csak a legutóbbi feltöltései vannak meg. Kattints a csatornakezelő megnyitásához (ezekre szűrve), és kérd le a teljes archívumukat.", diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 448d593..57e24b6 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -316,6 +316,7 @@ export interface MyStatus { quota_used_today: number; quota_remaining_today: number; paused: boolean; + sync_active: boolean; // a channel-sync job (backfill/rss) is running right now } export interface MyUsage {