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 { LS, useAccountPersistedState } from "../lib/storage";
import Tooltip from "./Tooltip";
import { Section, SettingRow } from "./ui/form";
const RANGES = [7, 30, 90] as const;
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 [tabRaw, setTab] = useAccountPersistedState(LS.statsTab, "overview");
// Clamp at render: only admins may land on the System tab (covers a stale stored value).
const tab: StatsTab = tabRaw === "system" && isAdmin ? "system" : "overview";
return (
{/* Only admins have a second (System) tab; everyone else just sees the Overview. */}
{isAdmin && (
{(["overview", "system"] as StatsTab[]).map((id) => (
setTab(id)}
className={`px-3 py-1.5 rounded-full text-sm border transition ${
tab === id
? "bg-accent text-accent-fg border-accent"
: "bg-card border-border text-muted hover:border-accent"
}`}
>
{t(`stats.tabs.${id}`)}
))}
)}
{tab === "system" && isAdmin ?
:
}
);
}
// 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 && (
syncSubs.mutate()}
disabled={syncSubs.isPending}
className="glass-card glass-hover flex items-center gap-2 px-3 py-2 rounded-xl text-sm disabled:opacity-50 transition"
>
{t("stats.sync.syncSubscriptions")}
deepAll.mutate()}
disabled={deepAll.isPending}
className="glass-card glass-hover flex items-center gap-2 px-3 py-2 rounded-xl text-sm disabled:opacity-50 transition mt-2"
>
{t("stats.sync.backfillEverything")}
)}
>
);
}
// 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 && (
pauseResume.mutate(s.paused)}
className="glass-card glass-hover flex items-center gap-2 px-3 py-2 rounded-xl text-sm transition"
>
{s.paused ? : }
{s.paused ? t("stats.sync.resumeBackgroundSync") : t("stats.sync.pauseBackgroundSync")}
)}
{t("stats.title")}
{RANGES.map((d) => (
setDays(d)}
className={`px-3 py-1.5 rounded-full text-sm border transition ${
days === d
? "bg-accent text-accent-fg border-accent"
: "bg-card border-border text-muted hover:border-accent"
}`}
>
{t("stats.rangeDays", { count: 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 (
setOpen((o) => !o)} className="w-full flex items-center gap-3 text-left">
{isSystem ? t("stats.system") : row.email}
{row.total.toLocaleString()}
{/* Proportion bar */}
{open && actions.length > 0 && (
{actions.map(([action, units]) => (
{quotaActionLabel(action)}
{units.toLocaleString()}
))}
)}
);
}