import { useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { qk } from "../lib/queryKeys"; import { useTranslation } from "react-i18next"; import { CheckCircle2, Clock, Database, History, Loader2, Pause, Play } from "lucide-react"; import { api, type MyStatus } from "../lib/api"; import { notifyActionError } from "../lib/actionErrors"; import { formatCompact } from "../lib/format"; import { PopoverSurface } from "./ui/AnchoredPopover"; import { useAnchoredPopover } from "./ui/anchoredPopoverHooks"; import { Button } from "./ui/Button"; import { IconButton } from "./ui/IconButton"; import { Pressable } from "./ui/Pressable"; import { SURFACE } from "./ui/surfaces"; // Per-user status (not the global catalog): shows the number of videos available to *this* // user, how many of *their* channels are still being fetched, and how many lack full // history (clickable -> channel manager filtered to those). The pause control is admin-only. export default function SyncStatus({ isAdmin, onGoToFullHistory, }: { isAdmin: boolean; onGoToFullHistory: () => void; }) { const { t, i18n } = useTranslation(); const lang = i18n.language; const qc = useQueryClient(); const [open, setOpen] = useState(false); // A read-out with a couple of controls rather than a list of choices, so it is a `dialog` // popover. It portals out of the header, whose glass backdrop-filter would otherwise trap its // stacking and positioning. const pop = useAnchoredPopover({ open, onOpenChange: setOpen, mode: "dialog", role: "dialog", placement: "bottom-end", offset: 8, }); const { setReference } = pop.refs; const { data } = useQuery({ queryKey: qk.myStatus(), queryFn: api.myStatus, // Track the scheduler near-real-time: poll even when the tab is backgrounded, and // refresh immediately on tab focus (the global default disables focus refetch), so the // "N without full history" count keeps ticking down without a manual reload. Poll faster // while a job is running or work is pending — so the live "syncing" state is actually // caught — and ease back to a slow idle tick once everything's settled. refetchInterval: (query) => { const d = query.state.data as MyStatus | undefined; const busy = !!d && (d.sync_active || d.channels_recent_pending > 0 || d.channels_deep_pending > 0); return busy ? 8_000 : 30_000; }, refetchIntervalInBackground: true, refetchOnWindowFocus: true, staleTime: 5_000, }); const toggle = useMutation({ mutationFn: () => (data?.paused ? api.resumeSync() : api.pauseSync()), onSuccess: () => qc.invalidateQueries({ queryKey: qk.myStatus() }), onError: (e) => notifyActionError( e, t(data?.paused ? "errors.writeFailed.syncResume" : "errors.writeFailed.syncPause"), ), }); if (!data) return null; 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; // Calm one-liner describing the current sync state. const stateNode = data.paused ? ( {t("header.sync.paused")} ) : active ? ( {syncing > 0 ? t("header.sync.syncing", { count: syncing }) : notFull > 0 ? t("header.sync.backfillingHistory") : t("header.sync.working")} ) : syncing > 0 ? ( {t("header.sync.recentQueued", { count: syncing })} ) : ( // A small check anchors the "all synced" state so it doesn't read as orphaned text. {t("header.sync.allSynced")} ); const pauseBtn = isAdmin && (data.paused || syncing > 0 || notFull > 0) && ( toggle.mutate()} loading={toggle.isPending} label={data.paused ? t("header.sync.resume") : t("header.sync.pause")} > {data.paused ? : } ); // Chip: the headline count rides in the header; the state, the history link and pause live in a // popover. An accent dot flags the only two things worth interrupting you for (paused / missing // history) so the collapsed chip still tells you when to look. const needsAttention = data.paused || notFull > 0; const countsText = `${formatCompact(data.my_videos, lang)} ${t("header.sync.yours")} / ${formatCompact( data.total_videos, lang, )} ${t("header.sync.total")}`; return (
{active ? ( ) : ( )} {formatCompact(data.my_videos, lang)} {needsAttention && ( )}
{formatCompact(data.my_videos, lang)}{" "} {t("header.sync.yours")} / {formatCompact(data.total_videos, lang)} {t("header.sync.total")}
{showMain && (
{stateNode} {pauseBtn}
)} {notFull > 0 && (
{!showMain && pauseBtn}
)}
); }