{job.running
@@ -140,6 +220,13 @@ export default function Scheduler() {
onError: () => notify({ level: "error", message: t("scheduler.toggleFailed") }),
});
+ const intervalMut = useMutation({
+ mutationFn: (v: { jobId: string; minutes: number }) =>
+ api.updateSchedulerJob(v.jobId, v.minutes),
+ onSuccess: () => qc.invalidateQueries({ queryKey: ["scheduler"] }),
+ onError: () => notify({ level: "error", message: t("scheduler.intervalFailed") }),
+ });
+
if (q.isLoading && !data)
return
{t("scheduler.loading")}
;
if (!data)
@@ -195,9 +282,15 @@ export default function Scheduler() {
{/* Jobs */}
{t("scheduler.jobsTitle")}
+
{data.jobs.map((job) => (
-
+ intervalMut.mutate({ jobId: job.id, minutes })}
+ />
))}
diff --git a/frontend/src/i18n/locales/de/scheduler.json b/frontend/src/i18n/locales/de/scheduler.json
index e54aa38..293c752 100644
--- a/frontend/src/i18n/locales/de/scheduler.json
+++ b/frontend/src/i18n/locales/de/scheduler.json
@@ -22,6 +22,25 @@
"quotaUsed": "{{used}} / {{budget}} Einheiten heute verbraucht",
"quotaRemaining": "{{formatted}} übrig",
"quotaNote": "Gemeinsames YouTube-Data-API-Budget; Reset um Mitternacht US-Pazifik. Backfill tritt zurück, um Spielraum für interaktive Nutzung zu lassen.",
+ "minutes": "Min",
+ "editInterval": "Klicken, um das Intervall zu ändern",
+ "intervalFailed": "Intervall konnte nicht geändert werden",
+ "dot": {
+ "running": "läuft gerade",
+ "ok": "letzter Lauf OK",
+ "idle": "untätig (in dieser Sitzung noch nicht gelaufen)",
+ "error": "letzter Lauf fehlgeschlagen",
+ "skipped": "übersprungen (Sync pausiert)"
+ },
+ "jobDesc": {
+ "rss_poll": "Prüft den RSS-Feed jedes Kanals auf neue Uploads (kostenlos, kein Kontingent). Fällt er aus, erscheinen neue Videos erst später, wenn ein Backfill sie erfasst — der Feed hinkt YouTube hinterher.",
+ "enrich": "Holt Videodetails (Dauer, Aufrufe, Live-/Shorts-Flags) und prüft Live-Videos erneut, bis sie enden. Fällt er aus, bleiben Videos ohne Dauer/Statistik und beendete Streams hängen als „live“ fest.",
+ "backfill": "Holt neue Uploads und dann den gesamten Verlauf vorgemerkter Kanäle, innerhalb des gemeinsamen Kontingents. Fällt er aus, werden ältere Videos und der volle Verlauf nicht ergänzt.",
+ "autotag": "Erkennt Sprache/Themen jedes Kanals und vergibt automatische Tags. Fällt er aus, bekommen neue Kanäle keine Auto-Tags, sodass Sprach-/Themenfilter sie verfehlen.",
+ "shorts": "Prüft youtube.com/shorts, um Shorts zu markieren. Fällt er aus, werden Shorts im Feed nicht von normalen Videos getrennt.",
+ "subscriptions": "Importiert die YouTube-Abos jedes Nutzers neu. Fällt er aus, werden auf YouTube hinzugefügte oder entfernte Abos hier nicht übernommen.",
+ "playlist_sync": "Spiegelt die YouTube-Wiedergabelisten jedes Nutzers in die App. Fällt er aus, erscheinen Änderungen an deinen YouTube-Listen lokal nicht."
+ },
"jobs": {
"rss_poll": "RSS-Abfrage (neue Uploads)",
"enrich": "Anreicherung + Live-Aktualisierung",
diff --git a/frontend/src/i18n/locales/en/scheduler.json b/frontend/src/i18n/locales/en/scheduler.json
index 60bff5c..e0db313 100644
--- a/frontend/src/i18n/locales/en/scheduler.json
+++ b/frontend/src/i18n/locales/en/scheduler.json
@@ -22,6 +22,25 @@
"quotaUsed": "{{used}} / {{budget}} units used today",
"quotaRemaining": "{{formatted}} left",
"quotaNote": "Shared YouTube Data API budget; resets at midnight US Pacific. Backfill yields to leave headroom for interactive use.",
+ "minutes": "min",
+ "editInterval": "Click to change how often this runs",
+ "intervalFailed": "Couldn't change the interval",
+ "dot": {
+ "running": "running now",
+ "ok": "last run OK",
+ "idle": "idle (not run yet this session)",
+ "error": "last run failed",
+ "skipped": "skipped (sync paused)"
+ },
+ "jobDesc": {
+ "rss_poll": "Checks each channel's RSS feed for new uploads (free, no quota). If it stops, new videos only appear later when a backfill pass catches them, so your feed lags behind YouTube.",
+ "enrich": "Fetches video details (duration, views, live/Shorts flags) and re-checks live videos until they end. If it stops, videos stay without a duration/stats and ended livestreams stay stuck as “live”.",
+ "backfill": "Pulls in recent uploads, then the full back-catalogue for opted-in channels, within the shared quota. If it stops, older videos and full history don't fill in.",
+ "autotag": "Detects each channel's language/topics and applies automatic tags. If it stops, new channels get no auto tags, so language/topic filters miss them.",
+ "shorts": "Probes youtube.com/shorts to mark which videos are Shorts. If it stops, Shorts aren't separated from normal videos in the feed.",
+ "subscriptions": "Re-imports every user's YouTube subscriptions. If it stops, channels subscribed to or dropped on YouTube aren't reflected here.",
+ "playlist_sync": "Mirrors each user's YouTube playlists into the app. If it stops, changes to your YouTube playlists don't show up locally."
+ },
"jobs": {
"rss_poll": "RSS poll (new uploads)",
"enrich": "Enrichment + live refresh",
diff --git a/frontend/src/i18n/locales/hu/scheduler.json b/frontend/src/i18n/locales/hu/scheduler.json
index 485bf0e..bfedaf9 100644
--- a/frontend/src/i18n/locales/hu/scheduler.json
+++ b/frontend/src/i18n/locales/hu/scheduler.json
@@ -22,6 +22,25 @@
"quotaUsed": "{{used}} / {{budget}} egység ma felhasználva",
"quotaRemaining": "{{formatted}} maradt",
"quotaNote": "Közös YouTube Data API-keret; éjfélkor (US Pacific) nullázódik. A backfill hátrébb sorol, hogy maradjon hely az interaktív használatnak.",
+ "minutes": "perc",
+ "editInterval": "Kattints a gyakoriság módosításához",
+ "intervalFailed": "Nem sikerült módosítani a gyakoriságot",
+ "dot": {
+ "running": "most fut",
+ "ok": "utolsó futás OK",
+ "idle": "tétlen (ebben a munkamenetben még nem futott)",
+ "error": "utolsó futás hibázott",
+ "skipped": "kihagyva (szinkron szüneteltetve)"
+ },
+ "jobDesc": {
+ "rss_poll": "Az egyes csatornák RSS-feedjét nézi új feltöltésekért (ingyenes, nem fogyaszt kvótát). Ha leáll, az új videók csak később jelennek meg, amikor egy backfill behúzza őket — így a hírfolyam lemarad a YouTube mögött.",
+ "enrich": "Lekéri a videók adatait (hossz, megtekintés, élő/Shorts jelzők), és frissíti az élő videókat, amíg véget nem érnek. Ha leáll, a videók hossz/statisztika nélkül maradnak, a véget ért adások pedig „élő” állapotban ragadnak.",
+ "backfill": "Behúzza a friss feltöltéseket, majd a kijelölt csatornák teljes előzményét, a közös kvótán belül. Ha leáll, a régebbi videók és a teljes előzmény nem töltődik fel.",
+ "autotag": "Felismeri a csatornák nyelvét/témáit, és automatikus címkéket ad. Ha leáll, az új csatornák címke nélkül maradnak, így a nyelv/téma szűrők kihagyják őket.",
+ "shorts": "A youtube.com/shorts próbával jelöli, mely videók Shorts-ok. Ha leáll, a Shorts-ok nem különülnek el a normál videóktól a hírfolyamban.",
+ "subscriptions": "Újraimportálja minden felhasználó YouTube-feliratkozásait. Ha leáll, a YouTube-on felvett vagy törölt feliratkozások nem tükröződnek itt.",
+ "playlist_sync": "Tükrözi a felhasználók YouTube lejátszási listáit az appba. Ha leáll, a YouTube-listák változásai nem jelennek meg lokálisan."
+ },
"jobs": {
"rss_poll": "RSS-lekérdezés (új feltöltések)",
"enrich": "Adatdúsítás + élő frissítés",
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index 960bbe1..0615f19 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -434,6 +434,11 @@ export const api = {
myUsage: (): Promise
=> req("/api/quota/my-usage"),
adminQuota: (days = 30): Promise => req(`/api/quota/admin?days=${days}`),
schedulerStatus: (): Promise => req("/api/admin/scheduler"),
+ updateSchedulerJob: (jobId: string, intervalMinutes: number): Promise<{ id: string; interval_minutes: number }> =>
+ req(`/api/admin/scheduler/jobs/${jobId}`, {
+ method: "PATCH",
+ body: JSON.stringify({ interval_minutes: intervalMinutes }),
+ }),
// --- onboarding / admin ---
requestAccess: (email: string): Promise<{ status: string }> =>