import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { History, Pause, Play, RefreshCw } from "lucide-react"; import { api, type AdminQuotaRow, type Me } from "../lib/api"; import { formatEta, quotaActionLabel } from "../lib/format"; import { notify } from "../lib/notifications"; import Tooltip from "./Tooltip"; const RANGES = [7, 30, 90] as const; const STATS_TAB_KEY = "siftlode.statsTab"; type StatsTab = "overview" | "system"; // Usage & stats page. "Overview" is per-user (sync status + your own API usage + actions); // "System" is the admin-only instance-wide quota dashboard + background-sync control. The // per-user content used to live in Settings → Sync; it moved here so Settings stays config-only. export default function Stats({ me }: { me: Me }) { const { t } = useTranslation(); const isAdmin = me.role === "admin"; const [tab, setTabState] = useState(() => { const v = localStorage.getItem(STATS_TAB_KEY); return v === "system" && isAdmin ? "system" : "overview"; }); const setTab = (id: StatsTab) => { setTabState(id); localStorage.setItem(STATS_TAB_KEY, id); }; return (
{/* Only admins have a second (System) tab; everyone else just sees the Overview. */} {isAdmin && (
{(["overview", "system"] as StatsTab[]).map((id) => ( ))}
)} {tab === "system" && isAdmin ? : }
); } function Section({ title, children }: { title: string; children: React.ReactNode }) { return (
{title}
{children}
); } function Row({ label, hint, children }: { label: string; hint?: string; children: React.ReactNode }) { return (
{label} {children}
); } // Per-user view: sync status + your own API usage + (for real accounts) the manual sync actions. function Overview({ me }: { me: Me }) { const { t } = useTranslation(); const qc = useQueryClient(); const status = useQuery({ queryKey: ["my-status"], queryFn: api.myStatus }); const usage = useQuery({ queryKey: ["my-usage"], queryFn: api.myUsage }); const s = status.data; const syncSubs = useMutation({ mutationFn: () => api.syncSubscriptions(), onSuccess: (r: { subscriptions?: number }) => { qc.invalidateQueries({ queryKey: ["my-status"] }); qc.invalidateQueries({ queryKey: ["channels"] }); notify({ level: "success", message: t("stats.sync.synced", { count: r.subscriptions ?? 0 }) }); }, onError: () => notify({ level: "error", message: t("stats.sync.syncFailed") }), }); const deepAll = useMutation({ mutationFn: () => api.deepAll(true), onSuccess: (r: { updated?: number }) => { qc.invalidateQueries({ queryKey: ["my-status"] }); qc.invalidateQueries({ queryKey: ["channels"] }); notify({ level: "success", message: t("stats.sync.fullHistoryRequested", { count: r.updated ?? 0 }), }); }, onError: () => notify({ level: "error", message: t("stats.sync.fullHistoryFailed") }), }); return ( <>
{s ? (
{s.channels_total} {`${s.channels_recent_synced}/${s.channels_total}`} {`${s.channels_deep_done}/${s.channels_deep_requested}`} {s.deep_pending_count > 0 && ( {formatEta(s.deep_eta_seconds)} )} {s.my_videos.toLocaleString()} {s.quota_remaining_today.toLocaleString()}
) : (
{t("stats.sync.loading")}
)}
{usage.data ? ( <>
{usage.data.today.toLocaleString()} {usage.data.last_7d.toLocaleString()} {usage.data.last_30d.toLocaleString()} {usage.data.all_time.toLocaleString()}
{Object.keys(usage.data.by_action).length > 0 && (
{t("stats.sync.byAction")}
{Object.entries(usage.data.by_action) .sort((a, b) => b[1] - a[1]) .map(([action, units]) => (
{quotaActionLabel(action)} {units.toLocaleString()}
))}
)} ) : (
{t("stats.sync.noUsage")}
)}
{!me.is_demo && (
)} ); } // Admin-only view: instance-wide quota dashboard + the global background-sync pause toggle. function SystemStats() { const { t } = useTranslation(); const qc = useQueryClient(); const [days, setDays] = useState(30); const q = useQuery({ queryKey: ["admin-quota", days], queryFn: () => api.adminQuota(days) }); const status = useQuery({ queryKey: ["my-status"], queryFn: api.myStatus }); const pauseResume = useMutation({ mutationFn: (paused: boolean) => (paused ? api.resumeSync() : api.pauseSync()), onSuccess: () => qc.invalidateQueries({ queryKey: ["my-status"] }), }); const data = q.data; const maxDaily = Math.max(1, ...(data?.daily ?? []).map((d) => d.total)); const grandTotal = (data?.rows ?? []).reduce((s, r) => s + r.total, 0); const s = status.data; return ( <> {s && (
)}

{t("stats.title")}

{RANGES.map((d) => ( ))}

{t("stats.introBefore")} {t("stats.introSystem")} {t("stats.introAfter")}

{q.isLoading ? (
{t("stats.loading")}
) : !data ? (
{t("stats.noData")}
) : ( <> {/* Daily totals (instance-wide, Pacific days) */}
{t("stats.dailyTotal", { count: data.range_days, units: grandTotal.toLocaleString() })}
{data.daily.length === 0 ? (
{t("stats.noUsageInRange")}
) : (
{data.daily.map((d) => (
))}
)}
{/* Per-user breakdown */}
{data.rows.length === 0 ? (
{t("stats.noUsageInRange")}
) : ( data.rows.map((r) => ( )) )}
)} ); } function UserRow({ row, max }: { row: AdminQuotaRow; max: number }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); const actions = Object.entries(row.by_action).sort((a, b) => b[1] - a[1]); const isSystem = row.user_id === null; return (
{/* Proportion bar */}
{open && actions.length > 0 && (
{actions.map(([action, units]) => (
{quotaActionLabel(action)} {units.toLocaleString()}
))}
)}
); }