import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { qk } from "../lib/queryKeys";
import { History, Pause, Play, RefreshCw } from "lucide-react";
import { api, type AdminQuotaRow, type Me } from "../lib/api";
import { formatEta, formatNumber, quotaActionLabel } from "../lib/format";
import { notify } from "../lib/notifications";
import { notifyYouTubeActionError } from "../lib/youtubeErrors";
import { useWizard } from "./wizardContext";
import { LS, useAccountPersistedState } from "../lib/storage";
import Tooltip from "./ui/Tooltip";
import { LoadingState, StateMessage } from "./ui/QueryState";
import FilterChipGroup from "./ui/FilterChipGroup";
import Tabs from "./ui/Tabs";
import { Section, SettingRow } from "./ui/form";
import { useMe } from "../lib/useMe";
import { notifyActionError } from "../lib/actionErrors";
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() {
const me = useMe();
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 && (
({
id,
label: 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();
// The 403 case needs somewhere to go, or the toast names a fix the user cannot reach from here.
const { openWizard } = useWizard();
const status = useQuery({ queryKey: qk.myStatus(), queryFn: api.myStatus });
const usage = useQuery({ queryKey: qk.myUsage(), queryFn: api.myUsage });
const s = status.data;
const syncSubs = useMutation({
mutationFn: () => api.syncSubscriptions(),
onSuccess: (r: { subscriptions?: number }) => {
qc.invalidateQueries({ queryKey: qk.myStatus() });
qc.invalidateQueries({ queryKey: qk.channels() });
notify({
level: "success",
message: t("stats.sync.synced", { count: r.subscriptions ?? 0 }),
});
},
// The same helper and the same wizard handle as the Channels page: it translates the 403 into
// "connect YouTube" WITH a button that opens the wizard — instead of surfacing the route's bare
// English detail under a Hungarian UI — and delegates everything else to notifyActionError so
// the server's own reason wins, which is what makes the SEC-14 cooldown read as a pace limit
// rather than "Sync failed".
onError: (e) => notifyYouTubeActionError(e, t("stats.sync.syncFailed"), openWizard),
});
const deepAll = useMutation({
mutationFn: () => api.deepAll(true),
onSuccess: (r: { updated?: number }) => {
qc.invalidateQueries({ queryKey: qk.myStatus() });
qc.invalidateQueries({ queryKey: qk.channels() });
notify({
level: "success",
message: t("stats.sync.fullHistoryRequested", { count: r.updated ?? 0 }),
});
},
onError: (e) => notifyActionError(e, t("stats.sync.fullHistoryFailed")),
});
return (
<>
{s ? (