feat(stats): consolidate per-user sync status into the Stats page
The Settings > Sync tab moves into the Stats module: Stats is now a per-user page (Overview tab = sync status + your own API usage + manual actions) with the admin instance-wide quota dashboard + background-sync pause as an admin-only System tab. The Stats nav item is visible to all users (was admin-only); the Settings Sync tab is removed. Sync i18n strings move from settings.sync.* to stats.sync.* (EN/HU/DE).
This commit is contained in:
parent
dc70a978ed
commit
874520bd8a
10 changed files with 358 additions and 306 deletions
|
|
@ -1,10 +1,9 @@
|
|||
import { useCallback, useEffect, useState, useSyncExternalStore } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Bell, Check, History, Monitor, Pause, Play, RefreshCw, RotateCcw, Save, Trash2, User, UserPlus, X } from "lucide-react";
|
||||
import { Bell, Check, Monitor, RotateCcw, Save, Trash2, User, UserPlus, X } from "lucide-react";
|
||||
import { SCHEMES, type Scheme, type ThemePrefs } from "../lib/theme";
|
||||
import { api, type Invite, type Me } from "../lib/api";
|
||||
import { formatEta, quotaActionLabel } from "../lib/format";
|
||||
import Avatar from "./Avatar";
|
||||
import { notify, type NotifSettings } from "../lib/notifications";
|
||||
import Tooltip from "./Tooltip";
|
||||
|
|
@ -32,11 +31,10 @@ export interface PrefsController {
|
|||
saveState: PrefsSaveState;
|
||||
}
|
||||
|
||||
type TabId = "appearance" | "notifications" | "sync" | "account";
|
||||
type TabId = "appearance" | "notifications" | "account";
|
||||
const TABS: { id: TabId; icon: typeof Monitor }[] = [
|
||||
{ id: "appearance", icon: Monitor },
|
||||
{ id: "notifications", icon: Bell },
|
||||
{ id: "sync", icon: RefreshCw },
|
||||
{ id: "account", icon: User },
|
||||
];
|
||||
|
||||
|
|
@ -98,7 +96,6 @@ export default function SettingsPanel({
|
|||
<div className="flex-1 min-w-0 p-4">
|
||||
{tab === "appearance" && <Appearance prefs={prefs} />}
|
||||
{tab === "notifications" && <Notifications prefs={prefs} />}
|
||||
{tab === "sync" && <Sync me={me} />}
|
||||
{tab === "account" && <Account me={me} onOpenWizard={onOpenWizard} />}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -317,158 +314,6 @@ function Notifications({ prefs }: { prefs: PrefsController }) {
|
|||
);
|
||||
}
|
||||
|
||||
function Sync({ 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("settings.sync.synced", { count: r.subscriptions ?? 0 }),
|
||||
});
|
||||
},
|
||||
onError: () => notify({ level: "error", message: t("settings.sync.syncFailed") }),
|
||||
});
|
||||
const pauseResume = useMutation({
|
||||
mutationFn: (paused: boolean) => (paused ? api.resumeSync() : api.pauseSync()),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["my-status"] }),
|
||||
});
|
||||
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("settings.sync.fullHistoryRequested", { count: r.updated ?? 0 }),
|
||||
});
|
||||
},
|
||||
onError: () => notify({ level: "error", message: t("settings.sync.fullHistoryFailed") }),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section title={t("settings.sync.myStatus")}>
|
||||
{s ? (
|
||||
<div className="space-y-1.5 text-sm">
|
||||
<Row label={t("settings.sync.channels")} hint={t("settings.sync.channelsHint")}>
|
||||
{s.channels_total}
|
||||
</Row>
|
||||
<Row
|
||||
label={t("settings.sync.recentSynced")}
|
||||
hint={t("settings.sync.recentSyncedHint")}
|
||||
>
|
||||
{`${s.channels_recent_synced}/${s.channels_total}`}
|
||||
</Row>
|
||||
<Row
|
||||
label={t("settings.sync.fullHistory")}
|
||||
hint={t("settings.sync.fullHistoryHint")}
|
||||
>
|
||||
{`${s.channels_deep_done}/${s.channels_deep_requested}`}
|
||||
</Row>
|
||||
{s.deep_pending_count > 0 && (
|
||||
<Row
|
||||
label={t("settings.sync.fullHistoryEta")}
|
||||
hint={t("settings.sync.fullHistoryEtaHint", { count: s.deep_pending_count })}
|
||||
>
|
||||
{formatEta(s.deep_eta_seconds)}
|
||||
</Row>
|
||||
)}
|
||||
<Row label={t("settings.sync.myVideos")} hint={t("settings.sync.myVideosHint")}>
|
||||
{s.my_videos.toLocaleString()}
|
||||
</Row>
|
||||
<Row
|
||||
label={t("settings.sync.quotaLeft")}
|
||||
hint={t("settings.sync.quotaLeftHint")}
|
||||
>
|
||||
{s.quota_remaining_today.toLocaleString()}
|
||||
</Row>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-muted text-sm">{t("settings.sync.loading")}</div>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
<Section title={t("settings.sync.apiUsage")}>
|
||||
{usage.data ? (
|
||||
<>
|
||||
<div className="space-y-1.5 text-sm">
|
||||
<Row label={t("settings.sync.today")} hint={t("settings.sync.todayHint")}>
|
||||
{usage.data.today.toLocaleString()}
|
||||
</Row>
|
||||
<Row label={t("settings.sync.last7d")}>{usage.data.last_7d.toLocaleString()}</Row>
|
||||
<Row label={t("settings.sync.last30d")}>{usage.data.last_30d.toLocaleString()}</Row>
|
||||
<Row label={t("settings.sync.allTime")}>{usage.data.all_time.toLocaleString()}</Row>
|
||||
</div>
|
||||
{Object.keys(usage.data.by_action).length > 0 && (
|
||||
<div className="mt-2 pt-2 border-t border-border space-y-1 text-xs text-muted">
|
||||
<div className="uppercase tracking-wide">{t("settings.sync.byAction")}</div>
|
||||
{Object.entries(usage.data.by_action)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([action, units]) => (
|
||||
<div key={action} className="flex justify-between gap-2">
|
||||
<span>{quotaActionLabel(action)}</span>
|
||||
<span className="tabular-nums">{units.toLocaleString()}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-muted text-sm">{t("settings.sync.noUsage")}</div>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
{!me.is_demo && (
|
||||
<Section title={t("settings.sync.actions")}>
|
||||
<Tooltip hint={t("settings.sync.syncSubscriptionsHint")}>
|
||||
<button
|
||||
onClick={() => 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"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${syncSubs.isPending ? "animate-spin" : ""}`} />
|
||||
{t("settings.sync.syncSubscriptions")}
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip hint={t("settings.sync.backfillEverythingHint")}>
|
||||
<button
|
||||
onClick={() => 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"
|
||||
>
|
||||
<History className={`w-4 h-4 ${deepAll.isPending ? "animate-pulse" : ""}`} />
|
||||
{t("settings.sync.backfillEverything")}
|
||||
</button>
|
||||
</Tooltip>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{me.role === "admin" && s && (
|
||||
<Section title={t("settings.sync.admin")}>
|
||||
<Tooltip hint={t("settings.sync.adminPauseHint")}>
|
||||
<button
|
||||
onClick={() => 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 ? <Play className="w-4 h-4" /> : <Pause className="w-4 h-4" />}
|
||||
{s.paused
|
||||
? t("settings.sync.resumeBackgroundSync")
|
||||
: t("settings.sync.pauseBackgroundSync")}
|
||||
</button>
|
||||
</Tooltip>
|
||||
</Section>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AccessRow({
|
||||
title,
|
||||
granted,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue