import { useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import { useI18n } from "../i18n";
import { WorkbenchCardIcon } from "../components/WorkbenchCardIcon";
import { modulesInSection, type ModuleDefinition, type WorkbenchSection } from "../config/modules";
import { openOrFocusModule } from "../utils/moduleWindows";
import { useAuth } from "../auth/AuthContext";
import { fetchOpsTasks, fetchRuntimeMetrics } from "../services/api";
const SECTIONS: WorkbenchSection[] = ["monitoring", "operations", "system"];
function clampPct(n: number): number {
if (!Number.isFinite(n)) return 0;
return Math.max(0, Math.min(100, Math.round(n)));
}
function ratioPct(used: number, limit: number): number {
if (limit <= 0) return 0;
return clampPct((used / limit) * 100);
}
function loadHint(pct: number, t: (k: string) => string): string {
if (pct >= 75) return t("workbench.gauge.high");
if (pct >= 40) return t("workbench.gauge.normal");
return t("workbench.gauge.low");
}
function gaugeTone(pct: number): string {
if (pct >= 75) return "#f59e0b";
if (pct >= 40) return "#3b82f6";
return "#22c55e";
}
function formatBytes(n: number, locale: string): string {
const v = Math.max(0, Number(n) || 0);
const gib = 1024 ** 3;
const mib = 1024 ** 2;
if (v >= gib) {
const g = v / gib;
return locale.startsWith("zh") ? `${g.toFixed(1)} GiB` : `${g.toFixed(1)} GiB`;
}
return `${(v / mib).toFixed(0)} MiB`;
}
function Gauge({ label, hint, pct }: { label: string; hint: string; pct: number }) {
const tone = gaugeTone(pct);
const safe = clampPct(pct);
return (
);
}
export function WorkbenchPage() {
const { t, locale } = useI18n();
const { user, isAdmin, hasScope } = useAuth();
const metricsQuery = useQuery({
queryKey: ["runtimeMetrics"],
queryFn: fetchRuntimeMetrics,
refetchInterval: 5000,
staleTime: 2000,
});
const opsQuery = useQuery({
queryKey: ["opsTasks"],
queryFn: fetchOpsTasks,
refetchInterval: 4000,
staleTime: 1500,
});
const visibleBySection = useMemo(() => {
const canSee = (mod: ModuleDefinition) =>
!mod.workbenchHidden &&
(!mod.adminOnly || isAdmin) &&
(!mod.requiredScope || hasScope(mod.requiredScope) || isAdmin);
return Object.fromEntries(
SECTIONS.map((section) => [section, modulesInSection(section).filter(canSee)]),
) as Record;
}, [isAdmin, hasScope]);
const metrics = metricsQuery.data;
const host = metrics?.host;
const activeTasks = opsQuery.data?.active ?? 0;
const totalTasks = opsQuery.data?.total ?? 0;
const cpuPct = clampPct(Number(host?.cpu_percent ?? 0));
const memPct = clampPct(Number(host?.mem_percent ?? 0));
const memUsed = Number(host?.mem_used_bytes || 0);
const memTotal = Number(host?.mem_total_bytes || 0);
const cliUsed = Number(metrics?.cli_budget?.in_use || 0);
const cliLimit = Math.max(1, Number(metrics?.cli_budget?.limit || 0));
const cliPct = ratioPct(cliUsed, cliLimit);
const dbOut = Number(metrics?.db_pool?.checked_out || 0);
const dbSize = Math.max(1, Number(metrics?.db_pool?.size || 0));
const dbPct = ratioPct(dbOut, dbSize);
const webUsed = Number(metrics?.webcrt?.active_sessions || 0);
const webMax = Math.max(1, Number(metrics?.webcrt?.max_sessions || 20));
const webPct = ratioPct(webUsed, webMax);
const taskCap = Math.max(totalTasks, activeTasks, 8);
const taskPct = ratioPct(activeTasks, taskCap);
const todayLabel = useMemo(() => {
try {
return new Intl.DateTimeFormat(locale === "en" ? "en-US" : "zh-CN", {
year: "numeric",
month: "long",
day: "numeric",
weekday: "short",
}).format(new Date());
} catch {
return new Date().toLocaleDateString();
}
}, [locale]);
const displayName = user?.username || "admin";
return (
{t("workbench.welcomeEyebrow")}
{t("workbench.welcomeTitle", { name: displayName })}
{t("workbench.today", { date: todayLabel })}
0
? `${formatBytes(memUsed, locale)} / ${formatBytes(memTotal, locale)}`
: loadHint(memPct, t)
}
pct={memPct}
/>
{SECTIONS.map((section) => {
const mods = visibleBySection[section];
if (mods.length === 0) return null;
return (
{t(`workbench.${section}`)}
{mods.length}
{mods.map((mod) => (
))}
);
})}
);
}