feat(dashboard): more System History metrics, persistence & localized labels

- Sample swap %, TCP/UDP connection counts and disk-usage % on the host ticker
- System History: Swap overlaid on the RAM tab, plus new Connections and Disk Usage tabs
- Persist the host time-series across restarts: gob snapshot beside the DB, written on a timer and at shutdown, restored on boot
- Live-refresh the open chart (2s for short ranges, 10s for longer)
- Localize CPU/RAM/Swap and the new tab/chart titles across all 13 languages and route legend series names through i18n
This commit is contained in:
MHSanaei
2026-06-03 12:16:31 +02:00
parent 4b11c54206
commit d4c020f365
18 changed files with 203 additions and 33 deletions
@@ -3,12 +3,14 @@ import type { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import { Modal, Select, Tabs } from 'antd';
import {
ApiOutlined,
DashboardOutlined,
DatabaseOutlined,
DeploymentUnitOutlined,
GlobalOutlined,
HddOutlined,
LineChartOutlined,
PieChartOutlined,
TeamOutlined,
} from '@ant-design/icons';
@@ -43,11 +45,13 @@ interface MetricDef {
}
const METRICS: MetricDef[] = [
{ key: 'cpu', tab: 'CPU', title: 'pages.index.historyTitleCpu', icon: <DashboardOutlined />, valueMax: 100, unit: '%', stroke: '' },
{ key: 'mem', tab: 'RAM', title: 'pages.index.historyTitleMem', icon: <DatabaseOutlined />, valueMax: 100, unit: '%', stroke: '#7c4dff' },
{ key: 'cpu', tab: 'CPU', tabKey: 'pages.index.cpu', title: 'pages.index.historyTitleCpu', icon: <DashboardOutlined />, valueMax: 100, unit: '%', stroke: '' },
{ key: 'mem', tab: 'RAM', tabKey: 'pages.index.memory', title: 'pages.index.historyTitleMem', icon: <DatabaseOutlined />, valueMax: 100, unit: '%', stroke: '#7c4dff', key2: 'swap', stroke2: '#ffa940', name1: 'pages.index.memory', name2: 'pages.index.swap' },
{ key: 'netUp', tab: 'Bandwidth', tabKey: 'pages.index.historyTabBandwidth', title: 'pages.index.historyTitleNetwork', icon: <GlobalOutlined />, valueMax: null, unit: 'B/s', stroke: '#1890ff', key2: 'netDown', stroke2: '#13c2c2', name1: 'Up', name2: 'Down' },
{ key: 'pktUp', tab: 'Packets', tabKey: 'pages.index.historyTabPackets', title: 'pages.index.historyTitlePackets', icon: <DeploymentUnitOutlined />, valueMax: null, unit: 'pkt/s', stroke: '#2f54eb', key2: 'pktDown', stroke2: '#36cfc9', name1: 'Up', name2: 'Down' },
{ key: 'tcpCount', tab: 'Connections', tabKey: 'pages.index.historyTabConnections', title: 'pages.index.historyTitleConnections', icon: <ApiOutlined />, valueMax: null, unit: '', stroke: '#597ef7', key2: 'udpCount', stroke2: '#73d13d', name1: 'TCP', name2: 'UDP' },
{ key: 'diskRead', tab: 'Disk I/O', tabKey: 'pages.index.historyTabDisk', title: 'pages.index.historyTitleDisk', icon: <HddOutlined />, valueMax: null, unit: 'B/s', stroke: '#eb2f96', key2: 'diskWrite', stroke2: '#722ed1', name1: 'Read', name2: 'Write' },
{ key: 'diskUsage', tab: 'Disk Usage', tabKey: 'pages.index.historyTabDiskUsage', title: 'pages.index.historyTitleDiskUsage', icon: <PieChartOutlined />, valueMax: 100, unit: '%', stroke: '#13c2c2' },
{ key: 'online', tab: 'Online', tabKey: 'pages.index.historyTabOnline', title: 'pages.index.historyTitleOnline', icon: <TeamOutlined />, valueMax: null, unit: '', stroke: '#52c41a' },
{ key: 'load1', tab: 'Load', tabKey: 'pages.index.historyTabLoad', title: 'pages.index.historyTitleLoad', icon: <LineChartOutlined />, valueMax: null, unit: '', stroke: '#fa8c16', key2: 'load5', stroke2: '#f5222d', name1: '1m', name2: '5m', key3: 'load15', stroke3: '#a0d911', name3: '15m' },
];
@@ -64,7 +68,9 @@ function unitFormatter(unit: string, activeKey: string): (v: number) => string {
}
return (v) => {
const n = Number(v) || 0;
if (activeKey === 'online') return String(Math.round(n));
if (activeKey === 'online' || activeKey === 'tcpCount' || activeKey === 'udpCount') {
return Math.round(n).toLocaleString();
}
return n.toFixed(2);
};
}
@@ -97,6 +103,7 @@ export default function SystemHistoryModal({ open, status, onClose }: SystemHist
const [timestamps, setTimestamps] = useState<number[]>([]);
const activeMetric = useMemo(() => METRICS.find((m) => m.key === activeKey), [activeKey]);
const trName = (n?: string) => (n && n.startsWith('pages.') ? t(n) : n);
const strokeColor = activeMetric?.stroke || status?.cpu?.color || '#008771';
const yFormatter = useMemo(
() => unitFormatter(activeMetric?.unit ?? '', activeKey),
@@ -178,6 +185,13 @@ export default function SystemHistoryModal({ open, status, onClose }: SystemHist
if (open) fetchBucket();
}, [open, activeKey, bucket, fetchBucket]);
useEffect(() => {
if (!open) return undefined;
const ms = bucket <= 30 ? 2000 : 10000;
const id = window.setInterval(() => fetchBucket(), ms);
return () => window.clearInterval(id);
}, [open, bucket, fetchBucket]);
return (
<Modal
open={open}
@@ -226,9 +240,9 @@ export default function SystemHistoryModal({ open, status, onClose }: SystemHist
data3={activeMetric?.key3 ? points3 : undefined}
stroke2={activeMetric?.stroke2}
stroke3={activeMetric?.stroke3}
name1={activeMetric?.name1}
name2={activeMetric?.name2}
name3={activeMetric?.name3}
name1={trName(activeMetric?.name1)}
name2={trName(activeMetric?.name2)}
name3={trName(activeMetric?.name3)}
labels={labels}
height={260}
stroke={strokeColor}