Files
fusion/packages/dashboard/app/components/SystemStatsModal.tsx
gsxdsm a2e96b132a feat(i18n): round-2 sweep — 488 residual strings migrated, markup fidelity restored (#1352)
- 51 fix agents covered every dirty batch from the round-1 verifiers:
  helper-function labels (roles, statuses, relative time), constant
  label maps (SETTINGS_SECTIONS, PROVIDER_INFO, EVENT_TYPE_LABELS),
  TUI help overlay + tab labels, toasts, placeholders, aria-labels
- Inline markup flattened by round 1 restored with <Trans>
  (DbCorruptionBanner storage-docs link, UpdateAvailableBanner code chip)
- Catalogs merged: +527 en keys across 5 locales; CLI bundles + app
  locale tree regenerated (6 locales)
- All 23 sweep-caused test regressions fixed: delta vs the clean-main
  baseline is now zero (remaining 4 local failures reproduce identically
  on origin/main; CI-green upstream)
- typecheck/lint clean; TUI 82/82, core locale 9/9

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 20:52:44 -07:00

524 lines
24 KiB
TypeScript

import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Monitor, RefreshCw, ShieldAlert, Skull, X } from "lucide-react";
import {
fetchGlobalSettings,
fetchSystemStats,
killVitestProcesses,
updateGlobalSettings,
type KillVitestResponse,
type SystemStatsResponse,
} from "../api";
import "./SystemStatsModal.css";
interface SystemStatsModalProps {
isOpen: boolean;
onClose: () => void;
projectId?: string;
}
function formatBytes(bytes: number): string {
if (!Number.isFinite(bytes) || bytes < 0) return "—";
const mb = bytes / (1024 * 1024);
if (mb < 1024) return `${mb.toFixed(0)} MB`;
return `${(mb / 1024).toFixed(2)} GB`;
}
function toPercent(used: number, total: number): string {
if (!Number.isFinite(used) || !Number.isFinite(total) || total <= 0) return "—";
return `${((used / total) * 100).toFixed(1)}%`;
}
type Severity = "normal" | "warning" | "critical";
function heapSeverity(used: number, limit: number): Severity {
if (limit <= 0) return "normal";
const pct = used / limit;
if (pct >= 0.85) return "critical";
if (pct >= 0.65) return "warning";
return "normal";
}
function rssSeverity(rss: number, totalSystemMem: number): Severity {
if (totalSystemMem <= 0) return "normal";
const pct = rss / totalSystemMem;
if (pct >= 0.5) return "critical";
if (pct >= 0.25) return "warning";
return "normal";
}
function systemMemSeverity(used: number, total: number): Severity {
if (total <= 0) return "normal";
const pct = used / total;
if (pct >= 0.9) return "critical";
if (pct >= 0.75) return "warning";
return "normal";
}
function cpuSeverity(percent: number | null, cores: number): Severity {
if (percent === null || !Number.isFinite(percent) || percent < 0) return "normal";
const normalized = cores > 0 ? percent / cores : percent;
if (normalized >= 80) return "critical";
if (normalized >= 50) return "warning";
return "normal";
}
function severityClassName(severity: Severity): string {
if (severity === "critical") return "system-stats-modal__value--critical";
if (severity === "warning") return "system-stats-modal__value--warning";
return "";
}
function formatTimestamp(value: string | null | undefined, notYetLabel: string): string {
if (!value) return notYetLabel;
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) return notYetLabel;
return parsed.toLocaleString();
}
/**
* SystemStatsModal groups dashboard runtime telemetry into five sections:
* process memory metrics, CPU/load information, host memory usage, task counts
* by column, and agent state counts.
*/
export function SystemStatsModal({ isOpen, onClose, projectId }: SystemStatsModalProps) {
const { t } = useTranslation("app");
const [stats, setStats] = useState<SystemStatsResponse | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [autoKillEnabled, setAutoKillEnabled] = useState(true);
const [killThreshold, setKillThreshold] = useState(90);
const [isKilling, setIsKilling] = useState(false);
const [confirmKill, setConfirmKill] = useState(false);
const [killResult, setKillResult] = useState<KillVitestResponse | null>(null);
const [settingsError, setSettingsError] = useState<string | null>(null);
const [lastRefreshedAt, setLastRefreshedAt] = useState<number | null>(null);
const loadStats = useCallback(async (options?: { preserveKillResult?: boolean }) => {
setLoading(true);
try {
const response = await fetchSystemStats(projectId);
setStats(response);
setError(null);
setLastRefreshedAt(Date.now());
if (!options?.preserveKillResult) {
setKillResult(null);
}
} catch (err) {
setError(err instanceof Error ? err.message : t("systemStats.errorLoadStats", "Failed to load system stats"));
} finally {
setLoading(false);
}
}, [projectId]);
useEffect(() => {
if (!isOpen) return;
void loadStats();
const timer = window.setInterval(() => {
void loadStats();
}, 5_000);
return () => {
window.clearInterval(timer);
};
}, [isOpen, loadStats]);
useEffect(() => {
if (!isOpen) return;
const loadSettings = async () => {
try {
const settings = await fetchGlobalSettings();
setAutoKillEnabled(settings.vitestAutoKillEnabled ?? true);
setKillThreshold(settings.vitestKillThresholdPct ?? 90);
setSettingsError(null);
} catch (err) {
setSettingsError(err instanceof Error ? err.message : t("systemStats.errorLoadVitestSettings", "Failed to load vitest settings"));
}
};
void loadSettings();
}, [isOpen]);
useEffect(() => {
if (!isOpen) return;
const onKeydown = (event: KeyboardEvent) => {
if (event.key === "Escape") onClose();
};
document.addEventListener("keydown", onKeydown);
return () => document.removeEventListener("keydown", onKeydown);
}, [isOpen, onClose]);
const processRows = useMemo(() => {
if (!stats) return [];
const system = stats.systemStats;
const heapClassName = severityClassName(heapSeverity(system.heapUsed, system.heapLimit));
const rssClassName = severityClassName(rssSeverity(system.rss, system.systemTotalMem));
return [
{ label: t("systemStats.rowRss", "RSS"), value: formatBytes(system.rss), detail: toPercent(system.rss, system.systemTotalMem), className: rssClassName },
{ label: t("systemStats.rowHeapUsed", "Heap Used"), value: formatBytes(system.heapUsed), detail: t("systemStats.rowHeapUsedDetail", "of {{total}}", { total: formatBytes(system.heapTotal) }), className: heapClassName },
{ label: t("systemStats.rowHeapLimit", "Heap Limit"), value: formatBytes(system.heapLimit), detail: t("systemStats.rowHeapLimitDetail", "V8 limit") },
{ label: t("systemStats.rowExternal", "External"), value: formatBytes(system.external) },
{ label: t("systemStats.rowArrayBuffers", "Array Buffers"), value: formatBytes(system.arrayBuffers) },
];
}, [stats]);
const persistAutoKill = useCallback(async (enabled: boolean) => {
setAutoKillEnabled(enabled);
try {
await updateGlobalSettings({ vitestAutoKillEnabled: enabled });
setSettingsError(null);
} catch (err) {
setSettingsError(err instanceof Error ? err.message : t("systemStats.errorSaveVitestSettings", "Failed to save vitest settings"));
}
}, []);
const persistKillThreshold = useCallback(async (nextThreshold: number) => {
const clamped = Math.min(99, Math.max(50, Number.isFinite(nextThreshold) ? Math.round(nextThreshold) : 90));
setKillThreshold(clamped);
try {
await updateGlobalSettings({ vitestKillThresholdPct: clamped });
setSettingsError(null);
} catch (err) {
setSettingsError(err instanceof Error ? err.message : t("systemStats.errorSaveVitestSettings", "Failed to save vitest settings"));
}
}, []);
const handleKillVitest = useCallback(async () => {
if (isKilling) return;
if (!confirmKill) {
setConfirmKill(true);
return;
}
setIsKilling(true);
try {
const result = await killVitestProcesses(projectId);
setKillResult(result);
setConfirmKill(false);
await loadStats({ preserveKillResult: true });
} catch (err) {
setError(err instanceof Error ? err.message : t("systemStats.errorKillVitest", "Failed to kill vitest processes"));
} finally {
setIsKilling(false);
}
}, [confirmKill, isKilling, loadStats, projectId]);
if (!isOpen) return null;
const system = stats?.systemStats;
const isBackgroundRefreshing = loading && Boolean(stats);
const refreshLabel = lastRefreshedAt
? t("systemStats.updatedAt", "Updated {{time}}", {
time: new Date(lastRefreshedAt).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}),
})
: t("systemStats.waitingFirstUpdate", "Waiting for first update");
const taskStats = stats?.taskStats;
const usedSystemMem = system ? system.systemTotalMem - system.systemFreeMem : 0;
const usedSystemMemSeverity = system ? systemMemSeverity(usedSystemMem, system.systemTotalMem) : "normal";
const usedSystemClassName = system ? severityClassName(usedSystemMemSeverity) : "";
const usedSystemMemPercent =
system && Number.isFinite(system.systemTotalMem) && system.systemTotalMem > 0
? Math.max(0, Math.min(100, (usedSystemMem / system.systemTotalMem) * 100))
: 0;
const usedSystemMemProgressLabel = system
? t("systemStats.systemMemProgressLabel", "System memory used: {{percent}}% ({{used}} of {{total}})", {
percent: usedSystemMemPercent.toFixed(1),
used: formatBytes(usedSystemMem),
total: formatBytes(system.systemTotalMem),
})
: t("systemStats.systemMemUnavailable", "System memory usage unavailable");
const vitestProcessCount = stats?.vitestProcessCount;
const cpuLoadSeverity = cpuSeverity(system?.cpuPercent ?? null, system?.cpuCount ?? 0);
const cpuClassName = severityClassName(cpuLoadSeverity);
const cpuPercentValue = system?.cpuPercent ?? null;
const cpuBarPercent = cpuPercentValue === null ? 0 : Math.max(0, Math.min(100, cpuPercentValue));
const cpuPercentLabel = cpuPercentValue === null ? t("systemStats.cpuSampling", "Sampling…") : `${cpuPercentValue.toFixed(1)}%`;
const cpuProgressLabel =
cpuPercentValue === null
? t("systemStats.cpuProgressUnavailable", "App CPU usage unavailable: waiting for another sample")
: t("systemStats.cpuProgressLabel", "App CPU usage: {{percent}}%", { percent: cpuPercentValue.toFixed(1) });
const killResultClassName = killResult
? killResult.killed > 0
? "system-stats-modal__kill-result system-stats-modal__kill-result--success"
: "system-stats-modal__kill-result system-stats-modal__kill-result--error"
: "";
const lastAutoKillLabel = formatTimestamp(stats?.vitestLastAutoKillAt, t("systemStats.notYet", "Not yet"));
return (
<div
className="modal-overlay open"
role="dialog"
aria-modal="true"
aria-labelledby="system-stats-modal-title"
onClick={(event) => {
if (event.target === event.currentTarget) onClose();
}}
data-testid="system-stats-modal-overlay"
>
<div className="modal modal-lg system-stats-modal" data-testid="system-stats-modal">
<div className="modal-header system-stats-modal__header">
<h2 id="system-stats-modal-title" className="system-stats-modal__title">
<Monitor />
<span>{t("systemStats.title", "System Stats")}</span>
</h2>
<div className="system-stats-modal__header-actions">
<span className="system-stats-modal__auto-refresh" aria-live="polite">
<span>{t("systemStats.autoRefresh", "Auto-refresh · 5s")}</span>
<span className="system-stats-modal__auto-refresh-time">{refreshLabel}</span>
</span>
<button
type="button"
className="btn-icon"
onClick={() => void loadStats()}
title={t("systemStats.refreshTitle", "Refresh")}
aria-label={t("systemStats.refreshAriaLabel", "Refresh system stats")}
>
<RefreshCw
size={16}
className={isBackgroundRefreshing ? "system-stats-modal__refresh--spinning" : undefined}
/>
</button>
<button type="button" className="modal-close" onClick={onClose} aria-label={t("systemStats.closeAriaLabel", "Close")}>
<X />
</button>
</div>
</div>
{loading && !stats && <div className="system-stats-modal__state">{t("systemStats.loading", "Loading system stats…")}</div>}
{error && !stats && (
<div className="system-stats-modal__state system-stats-modal__state--error" role="alert">
{error}
</div>
)}
{stats && (
<div className="system-stats-modal__content">
<section className="system-stats-modal__section" aria-label={t("systemStats.sectionProcessAriaLabel", "Process stats")}>
<h3 className="system-stats-modal__section-title">{t("systemStats.sectionProcess", "Process")}</h3>
<dl className="system-stats-modal__grid">
{processRows.map((row) => (
<div key={row.label} className="system-stats-modal__row">
<dt>{row.label}</dt>
<dd>
<span className={`system-stats-modal__value ${row.className}`.trim()}>{row.value}</span>
{row.detail ? <span className="system-stats-modal__detail">{row.detail}</span> : null}
</dd>
</div>
))}
</dl>
</section>
<section className="system-stats-modal__section" aria-label={t("systemStats.sectionCpuAriaLabel", "CPU and load stats")}>
<h3 className="system-stats-modal__section-title">{t("systemStats.sectionCpu", "CPU & Load")}</h3>
<dl className="system-stats-modal__grid">
<div className="system-stats-modal__row system-stats-modal__row--cpu-used">
<dt>{t("systemStats.rowAppCpu", "App CPU")}</dt>
<dd>
<span className={`system-stats-modal__value ${cpuClassName}`.trim()}>{cpuPercentLabel}</span>
<span className="system-stats-modal__detail">{cpuPercentValue === null ? t("systemStats.cpuFirstSamplePending", "First sample pending") : t("systemStats.cpuProcessUsage", "process usage")}</span>
</dd>
<div className="system-stats-modal__memory-progress-wrapper">
<div
className={`system-stats-modal__memory-progress-track system-stats-modal__memory-progress-track--${cpuLoadSeverity}`}
role="progressbar"
aria-valuenow={Math.round(cpuBarPercent)}
aria-valuemin={0}
aria-valuemax={100}
aria-label={cpuProgressLabel}
>
<div
className={`system-stats-modal__memory-progress-fill system-stats-modal__memory-progress-fill--${cpuLoadSeverity}`}
style={{ width: `${cpuBarPercent}%` }}
/>
</div>
</div>
</div>
<div className="system-stats-modal__row">
<dt>{t("systemStats.rowLoadAvg", "Load Avg")}</dt>
<dd>{system?.loadAvg.map((value) => value.toFixed(2)).join(" ") ?? "—"}</dd>
</div>
<div className="system-stats-modal__row">
<dt>{t("systemStats.rowCores", "Cores")}</dt>
<dd>{system?.cpuCount ?? "—"}</dd>
</div>
<div className="system-stats-modal__row">
<dt>{t("systemStats.rowPlatform", "Platform")}</dt>
<dd>{system?.platform ?? "—"}</dd>
</div>
<div className="system-stats-modal__row">
<dt>{t("systemStats.rowNode", "Node")}</dt>
<dd>{system?.nodeVersion ?? "—"}</dd>
</div>
<div className="system-stats-modal__row">
<dt>{t("systemStats.rowPid", "PID")}</dt>
<dd>{system?.pid ?? "—"}</dd>
</div>
</dl>
</section>
<section className="system-stats-modal__section" aria-label={t("systemStats.sectionSystemMemAriaLabel", "System memory stats")}>
<h3 className="system-stats-modal__section-title">{t("systemStats.sectionSystem", "System")}</h3>
<dl className="system-stats-modal__grid">
<div className="system-stats-modal__row system-stats-modal__row--memory-used">
<dt>{t("systemStats.rowMemoryUsed", "Memory Used")}</dt>
<dd>
<span className={`system-stats-modal__value ${usedSystemClassName}`.trim()}>
{system ? formatBytes(usedSystemMem) : "—"}
</span>
<span className="system-stats-modal__detail">
{system ? `${toPercent(usedSystemMem, system.systemTotalMem)} of ${formatBytes(system.systemTotalMem)}` : ""}
</span>
</dd>
<div className="system-stats-modal__memory-progress-wrapper">
<div
className={`system-stats-modal__memory-progress-track system-stats-modal__memory-progress-track--${usedSystemMemSeverity}`}
role="progressbar"
aria-valuenow={Math.round(usedSystemMemPercent)}
aria-valuemin={0}
aria-valuemax={100}
aria-label={usedSystemMemProgressLabel}
>
<div
className={`system-stats-modal__memory-progress-fill system-stats-modal__memory-progress-fill--${usedSystemMemSeverity}`}
style={{ width: `${usedSystemMemPercent}%` }}
/>
</div>
</div>
</div>
<div className="system-stats-modal__row">
<dt>{t("systemStats.rowMemoryFree", "Memory Free")}</dt>
<dd>{system ? formatBytes(system.systemFreeMem) : "—"}</dd>
</div>
</dl>
</section>
<section className="system-stats-modal__section" aria-label={t("systemStats.sectionTasksAriaLabel", "Task stats")}>
<h3 className="system-stats-modal__section-title">{t("systemStats.sectionTasks", "Tasks")}</h3>
<dl className="system-stats-modal__grid">
<div className="system-stats-modal__row">
<dt>{t("systemStats.rowTotal", "Total")}</dt>
<dd>{taskStats?.total ?? 0}</dd>
</div>
{Object.entries(taskStats?.byColumn ?? {}).map(([column, count]) => (
<div key={column} className="system-stats-modal__row">
<dt>{column}</dt>
<dd>{count}</dd>
</div>
))}
</dl>
</section>
<section className="system-stats-modal__section" aria-label={t("systemStats.sectionAgentsAriaLabel", "Agent stats")}>
<h3 className="system-stats-modal__section-title">{t("systemStats.sectionAgents", "Agents")}</h3>
<dl className="system-stats-modal__grid">
<div className="system-stats-modal__row">
<dt>{t("systemStats.agentIdle", "idle")}</dt>
<dd>{taskStats?.agents.idle ?? 0}</dd>
</div>
<div className="system-stats-modal__row">
<dt>{t("systemStats.agentActive", "active")}</dt>
<dd>{taskStats?.agents.active ?? 0}</dd>
</div>
<div className="system-stats-modal__row">
<dt>{t("systemStats.agentRunning", "running")}</dt>
<dd>{taskStats?.agents.running ?? 0}</dd>
</div>
<div className="system-stats-modal__row">
<dt>{t("systemStats.agentError", "error")}</dt>
<dd>{taskStats?.agents.error ?? 0}</dd>
</div>
</dl>
</section>
<section className="system-stats-modal__section" aria-label={t("systemStats.sectionVitestAriaLabel", "Vitest controls")}>
<h3 className="system-stats-modal__section-title system-stats-modal__section-title--with-icon">
<ShieldAlert />
<span>{t("systemStats.sectionVitest", "Vitest Controls")}</span>
</h3>
<dl className="system-stats-modal__grid system-stats-modal__vitest-controls">
<div className="system-stats-modal__row">
<dt>{t("systemStats.vitestProcesses", "Vitest Processes")}</dt>
<dd>{vitestProcessCount ?? "—"}</dd>
</div>
</dl>
<div className="system-stats-modal__vitest-controls">
<div className="system-stats-modal__kill-row">
<button
type="button"
className="btn btn-danger"
onClick={() => void handleKillVitest()}
disabled={isKilling || vitestProcessCount === 0}
>
<Skull />
<span>{confirmKill ? t("systemStats.confirmKill", "Confirm Kill?") : t("systemStats.killVitest", "Kill Vitest Processes")}</span>
</button>
</div>
<label className="system-stats-modal__toggle-row">
<input
type="checkbox"
checked={autoKillEnabled}
onChange={(event) => {
void persistAutoKill(event.target.checked);
}}
/>
<span>{t("systemStats.autoKillLabel", "Auto-kill vitest on memory pressure")}</span>
</label>
<div className="system-stats-modal__threshold-row">
<label htmlFor="vitest-threshold-number">{t("systemStats.killThresholdLabel", "Kill threshold (%)")}</label>
<div className="system-stats-modal__threshold-controls">
<input
id="vitest-threshold-range"
type="range"
min={50}
max={99}
value={killThreshold}
aria-label={t("systemStats.killThresholdSliderAriaLabel", "Kill threshold slider (%)")}
onChange={(event) => {
const nextValue = Number.parseInt(event.target.value, 10);
void persistKillThreshold(Number.isNaN(nextValue) ? 90 : nextValue);
}}
/>
<input
id="vitest-threshold-number"
type="number"
className="input"
min={50}
max={99}
value={killThreshold}
aria-label={t("systemStats.killThresholdInputAriaLabel", "Kill threshold (%)")}
onChange={(event) => {
const nextValue = Number.parseInt(event.target.value, 10);
void persistKillThreshold(Number.isNaN(nextValue) ? 90 : nextValue);
}}
onBlur={() => {
void persistKillThreshold(killThreshold);
}}
/>
</div>
</div>
{killResult && <p className={killResultClassName}>{t("systemStats.killedProcesses", "Killed {{count}} processes", { count: killResult.killed })}</p>}
<p className="system-stats-modal__last-kill">{t("systemStats.lastAutoKill", "Last auto-kill: {{time}}", { time: lastAutoKillLabel })}</p>
{settingsError && <p className="system-stats-modal__kill-result system-stats-modal__kill-result--error">{settingsError}</p>}
</div>
</section>
</div>
)}
{error && stats && <div className="system-stats-modal__footer-error">{t("systemStats.footerRefreshFailed", "Latest refresh failed: {{error}}", { error })}</div>}
</div>
</div>
);
}