feat(ui): E31 S4b — the admin pages behind one rail row, Configuration as a sectioned list, Statistics as tiles and bars

- NavRailGroup: one "Admin" rail row opening a panel of the admin pages (links, so a
  middle-click still opens a tab); the rail fits 133% zoom without a hidden scroll
- sysconfig.SECTIONS orders the config groups under four headings (every group in exactly
  one, test-held); the page shows them as Settings' side list (Tabs `group` headings);
  the lone registration group reads "Registration" under "Access"
- StatTiles / StatTile / StatBars: the Statistics overview's figures as tiles, the
  per-operation quota use as horizontal bars (a zero draws none)
This commit is contained in:
2026-10-02 00:07:00 +02:00
parent 25905aa786
commit be156fc52b
21 changed files with 753 additions and 223 deletions
+63 -47
View File
@@ -15,7 +15,9 @@ import FilterChipGroup from "./ui/FilterChipGroup";
import Tabs from "./ui/Tabs";
import { useTabPanel } from "./ui/tabPanel";
import { PageToolbar } from "./ui/PageShell";
import { Section, SettingRow } from "./ui/form";
import { Section } from "./ui/form";
import { SectionLabel } from "./ui/Headings";
import { StatBars, StatTile, StatTiles } from "./ui/StatTiles";
import { useMe } from "../lib/useMe";
import { notifyActionError } from "../lib/actionErrors";
import { Button } from "./ui/Button";
@@ -108,34 +110,40 @@ function Overview({ me }: { me: Me }) {
<>
<Section title={t("stats.sync.myStatus")}>
{s ? (
<div className="space-y-1.5 text-sm">
<SettingRow label={t("stats.sync.channels")} hint={t("stats.sync.channelsHint")}>
{s.channels_total}
</SettingRow>
<SettingRow
<StatTiles>
<StatTile
label={t("stats.sync.channels")}
hint={t("stats.sync.channelsHint")}
value={formatNumber(s.channels_total, lang)}
/>
<StatTile
label={t("stats.sync.recentSynced")}
hint={t("stats.sync.recentSyncedHint")}
>
{`${s.channels_recent_synced}/${s.channels_total}`}
</SettingRow>
<SettingRow label={t("stats.sync.fullHistory")} hint={t("stats.sync.fullHistoryHint")}>
{`${s.channels_deep_done}/${s.channels_deep_requested}`}
</SettingRow>
value={`${formatNumber(s.channels_recent_synced, lang)}/${formatNumber(s.channels_total, lang)}`}
/>
<StatTile
label={t("stats.sync.fullHistory")}
hint={t("stats.sync.fullHistoryHint")}
value={`${formatNumber(s.channels_deep_done, lang)}/${formatNumber(s.channels_deep_requested, lang)}`}
/>
{s.deep_pending_count > 0 && (
<SettingRow
<StatTile
label={t("stats.sync.fullHistoryEta")}
hint={t("stats.sync.fullHistoryEtaHint", { count: s.deep_pending_count })}
>
{formatEta(t, s.deep_eta_seconds)}
</SettingRow>
value={formatEta(t, s.deep_eta_seconds)}
/>
)}
<SettingRow label={t("stats.sync.myVideos")} hint={t("stats.sync.myVideosHint")}>
{formatNumber(s.my_videos, lang)}
</SettingRow>
<SettingRow label={t("stats.sync.quotaLeft")} hint={t("stats.sync.quotaLeftHint")}>
{formatNumber(s.quota_remaining_today, lang)}
</SettingRow>
</div>
<StatTile
label={t("stats.sync.myVideos")}
hint={t("stats.sync.myVideosHint")}
value={formatNumber(s.my_videos, lang)}
/>
<StatTile
label={t("stats.sync.quotaLeft")}
hint={t("stats.sync.quotaLeftHint")}
value={formatNumber(s.quota_remaining_today, lang)}
/>
</StatTiles>
) : status.isError ? (
<StateMessage tone="error" onRetry={() => status.refetch()}>
{t("common.loadError")}
@@ -148,31 +156,39 @@ function Overview({ me }: { me: Me }) {
<Section title={t("stats.sync.apiUsage")}>
{usage.data ? (
<>
<div className="space-y-1.5 text-sm">
<SettingRow label={t("stats.sync.today")} hint={t("stats.sync.todayHint")}>
{formatNumber(usage.data.today, lang)}
</SettingRow>
<SettingRow label={t("stats.sync.last7d")}>
{formatNumber(usage.data.last_7d, lang)}
</SettingRow>
<SettingRow label={t("stats.sync.last30d")}>
{formatNumber(usage.data.last_30d, lang)}
</SettingRow>
<SettingRow label={t("stats.sync.allTime")}>
{formatNumber(usage.data.all_time, lang)}
</SettingRow>
</div>
<StatTiles>
<StatTile
label={t("stats.sync.today")}
hint={t("stats.sync.todayHint")}
value={formatNumber(usage.data.today, lang)}
/>
<StatTile
label={t("stats.sync.last7d")}
value={formatNumber(usage.data.last_7d, lang)}
/>
<StatTile
label={t("stats.sync.last30d")}
value={formatNumber(usage.data.last_30d, lang)}
/>
<StatTile
label={t("stats.sync.allTime")}
value={formatNumber(usage.data.all_time, lang)}
/>
</StatTiles>
{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("stats.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(t, action)}</span>
<span className="tabular-nums">{formatNumber(units, lang)}</span>
</div>
))}
<div className="mt-4">
<SectionLabel level={3} className="mb-2">
{t("stats.sync.byAction")}
</SectionLabel>
<StatBars
label={t("stats.sync.byAction")}
format={(n) => formatNumber(n, lang)}
rows={Object.entries(usage.data.by_action).map(([action, units]) => ({
id: action,
label: quotaActionLabel(t, action),
value: units,
}))}
/>
</div>
)}
</>