Browse Source

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
MHSanaei 17 hours ago
parent
commit
d4c020f365

+ 20 - 6
frontend/src/pages/index/SystemHistoryModal.tsx

@@ -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}

+ 6 - 0
web/controller/server.go

@@ -33,6 +33,7 @@ type ServerController struct {
 // NewServerController creates a new ServerController, initializes routes, and starts background tasks.
 func NewServerController(g *gin.RouterGroup) *ServerController {
 	a := &ServerController{}
+	service.RestoreSystemMetrics()
 	a.initRouter(g)
 	a.startTask()
 	return a
@@ -84,6 +85,11 @@ func (a *ServerController) startTask() {
 		a.xrayMetricsService.Sample(time.Now())
 		websocket.BroadcastStatus(status)
 	})
+	c.AddFunc("@every 1m", func() {
+		if err := service.PersistSystemMetrics(); err != nil {
+			logger.Warning("persist system metrics failed:", err)
+		}
+	})
 }
 
 // status returns the current server status information.

+ 86 - 1
web/service/metric_history.go

@@ -1,8 +1,14 @@
 package service
 
 import (
+	"encoding/gob"
+	"os"
+	"path/filepath"
 	"sync"
 	"time"
+
+	"github.com/mhsanaei/3x-ui/v3/config"
+	"github.com/mhsanaei/3x-ui/v3/logger"
 )
 
 // MetricSample is one point of any time-series we keep in memory.
@@ -59,6 +65,34 @@ func (h *metricHistory) drop(metric string) {
 	h.mu.Unlock()
 }
 
+// snapshot returns a deep copy of every series, safe to serialize without
+// holding the lock during disk I/O.
+func (h *metricHistory) snapshot() map[string][]MetricSample {
+	h.mu.Lock()
+	defer h.mu.Unlock()
+	out := make(map[string][]MetricSample, len(h.metrics))
+	for k, v := range h.metrics {
+		cp := make([]MetricSample, len(v))
+		copy(cp, v)
+		out[k] = cp
+	}
+	return out
+}
+
+// restore replaces the in-memory series with a previously persisted set,
+// re-applying the per-series capacity cap so a tampered or oversized file
+// can't grow the working set unbounded.
+func (h *metricHistory) restore(data map[string][]MetricSample) {
+	h.mu.Lock()
+	defer h.mu.Unlock()
+	for k, v := range data {
+		if len(v) > metricCapacityDefault {
+			v = v[len(v)-metricCapacityDefault:]
+		}
+		h.metrics[k] = v
+	}
+}
+
 // aggregate returns up to maxPoints buckets of size bucketSeconds,
 // each bucket carrying the arithmetic mean of the underlying samples.
 // Bucket alignment is to absolute Unix-second boundaries so two
@@ -137,7 +171,7 @@ var (
 // status sample. Exposed for documentation/test purposes; the
 // controller validates incoming names against an allow-list.
 var SystemMetricKeys = []string{
-	"cpu", "mem", "netUp", "netDown", "pktUp", "pktDown", "diskRead", "diskWrite", "online", "load1", "load5", "load15",
+	"cpu", "mem", "swap", "netUp", "netDown", "pktUp", "pktDown", "diskRead", "diskWrite", "diskUsage", "tcpCount", "udpCount", "online", "load1", "load5", "load15",
 }
 
 // NodeMetricKeys lists the per-node metric names NodeHeartbeatJob writes.
@@ -150,3 +184,54 @@ var NodeMetricKeys = []string{"cpu", "mem"}
 var XrayMetricKeys = []string{
 	"xrAlloc", "xrSys", "xrHeapObjects", "xrNumGC", "xrPauseNs",
 }
+
+// systemMetricsStorePath is where the host time-series is persisted between
+// restarts. It lives next to the database so a single volume mount carries
+// both. Only systemMetrics is persisted — node and xray series are cheap to
+// rebuild and tied to live connections.
+func systemMetricsStorePath() string {
+	return filepath.Join(config.GetDBFolderPath(), "system_metrics.gob")
+}
+
+// PersistSystemMetrics writes the host time-series to disk via a temp file +
+// rename so a crash mid-write can't corrupt the previous snapshot. Called on a
+// timer and at shutdown.
+func PersistSystemMetrics() error {
+	path := systemMetricsStorePath()
+	tmp := path + ".tmp"
+	f, err := os.Create(tmp)
+	if err != nil {
+		return err
+	}
+	if err := gob.NewEncoder(f).Encode(systemMetrics.snapshot()); err != nil {
+		f.Close()
+		os.Remove(tmp)
+		return err
+	}
+	if err := f.Close(); err != nil {
+		os.Remove(tmp)
+		return err
+	}
+	return os.Rename(tmp, path)
+}
+
+// RestoreSystemMetrics loads a previously persisted host time-series on startup.
+// A missing file is not an error (first boot). Aggregation already windows by
+// time, so any gap from downtime is handled by the readers.
+func RestoreSystemMetrics() {
+	path := systemMetricsStorePath()
+	f, err := os.Open(path)
+	if err != nil {
+		if !os.IsNotExist(err) {
+			logger.Warning("restore system metrics failed:", err)
+		}
+		return
+	}
+	defer f.Close()
+	var data map[string][]MetricSample
+	if err := gob.NewDecoder(f).Decode(&data); err != nil {
+		logger.Warning("decode system metrics failed:", err)
+		return
+	}
+	systemMetrics.restore(data)
+}

+ 10 - 0
web/service/server.go

@@ -565,12 +565,22 @@ func (s *ServerService) AppendStatusSample(t time.Time, status *Status) {
 	if status.Mem.Total > 0 {
 		systemMetrics.append("mem", t, float64(status.Mem.Current)*100.0/float64(status.Mem.Total))
 	}
+	if status.Swap.Total > 0 {
+		systemMetrics.append("swap", t, float64(status.Swap.Current)*100.0/float64(status.Swap.Total))
+	} else {
+		systemMetrics.append("swap", t, 0)
+	}
 	systemMetrics.append("netUp", t, float64(status.NetIO.Up))
 	systemMetrics.append("netDown", t, float64(status.NetIO.Down))
 	systemMetrics.append("diskRead", t, float64(status.DiskIO.Read))
 	systemMetrics.append("diskWrite", t, float64(status.DiskIO.Write))
+	if status.Disk.Total > 0 {
+		systemMetrics.append("diskUsage", t, float64(status.Disk.Current)*100.0/float64(status.Disk.Total))
+	}
 	systemMetrics.append("pktUp", t, float64(status.NetIO.PktUp))
 	systemMetrics.append("pktDown", t, float64(status.NetIO.PktDown))
+	systemMetrics.append("tcpCount", t, float64(status.TcpCount))
+	systemMetrics.append("udpCount", t, float64(status.UdpCount))
 	online := 0
 	if p != nil && p.IsRunning() {
 		online = len(p.GetOnlineClients())

+ 8 - 4
web/translation/ar-EG.json

@@ -128,12 +128,12 @@
     },
     "index": {
       "title": "نظرة عامة",
-      "cpu": "CPU",
+      "cpu": "المعالج",
       "logicalProcessors": "المعالجات المنطقية",
       "frequency": "التردد",
-      "swap": "Swap",
+      "swap": "التبديل",
       "storage": "تخزين",
-      "memory": "RAM",
+      "memory": "الذاكرة",
       "threads": "خيوط",
       "xrayStatus": "Xray",
       "stopXray": "إيقاف",
@@ -162,11 +162,15 @@
       "historyTitleDisk": "إدخال/إخراج القرص",
       "historyTitleOnline": "العملاء المتصلون",
       "historyTitleLoad": "متوسط حمل النظام (1 / 5 / 15 دقيقة)",
+      "historyTitleConnections": "الاتصالات النشطة (TCP / UDP)",
+      "historyTitleDiskUsage": "استخدام مساحة القرص",
       "historyTabBandwidth": "عرض النطاق",
       "historyTabPackets": "الحزم",
-      "historyTabDisk": "Disk I/O",
+      "historyTabDisk": "قرص I/O",
       "historyTabOnline": "متصل",
       "historyTabLoad": "الحِمل",
+      "historyTabConnections": "الاتصالات",
+      "historyTabDiskUsage": "استخدام القرص",
       "charts": "الرسوم البيانية",
       "xrayMetricsTitle": "مقاييس Xray",
       "xrayTitleHeap": "ذاكرة الكومة المخصصة",

+ 4 - 0
web/translation/en-US.json

@@ -162,11 +162,15 @@
       "historyTitleDisk": "Disk I/O",
       "historyTitleOnline": "Online Clients",
       "historyTitleLoad": "System Load Average (1m / 5m / 15m)",
+      "historyTitleConnections": "Active Connections (TCP / UDP)",
+      "historyTitleDiskUsage": "Disk Space Usage",
       "historyTabBandwidth": "Bandwidth",
       "historyTabPackets": "Packets",
       "historyTabDisk": "Disk I/O",
       "historyTabOnline": "Online",
       "historyTabLoad": "Load",
+      "historyTabConnections": "Connections",
+      "historyTabDiskUsage": "Disk Usage",
       "charts": "Charts",
       "xrayMetricsTitle": "Xray Metrics",
       "xrayTitleHeap": "Allocated Heap Memory",

+ 5 - 1
web/translation/es-ES.json

@@ -133,7 +133,7 @@
       "frequency": "Frecuencia",
       "swap": "Swap",
       "storage": "Almacenamiento",
-      "memory": "RAM",
+      "memory": "Memoria",
       "threads": "Hilos",
       "xrayStatus": "Xray",
       "stopXray": "Detener",
@@ -162,11 +162,15 @@
       "historyTitleDisk": "E/S de Disco",
       "historyTitleOnline": "Clientes en Línea",
       "historyTitleLoad": "Carga Media del Sistema (1 / 5 / 15 min)",
+      "historyTitleConnections": "Conexiones Activas (TCP / UDP)",
+      "historyTitleDiskUsage": "Uso del Espacio en Disco",
       "historyTabBandwidth": "Ancho de Banda",
       "historyTabPackets": "Paquetes",
       "historyTabDisk": "Disco I/O",
       "historyTabOnline": "En línea",
       "historyTabLoad": "Carga",
+      "historyTabConnections": "Conexiones",
+      "historyTabDiskUsage": "Uso de Disco",
       "charts": "Gráficos",
       "xrayMetricsTitle": "Métricas de Xray",
       "xrayTitleHeap": "Memoria Heap Asignada",

+ 8 - 4
web/translation/fa-IR.json

@@ -128,12 +128,12 @@
     },
     "index": {
       "title": "نمای کلی",
-      "cpu": "CPU",
+      "cpu": "پردازنده",
       "logicalProcessors": "پردازنده‌های منطقی",
       "frequency": "فرکانس",
-      "swap": "Swap",
+      "swap": "سواپ",
       "storage": "ذخیره‌سازی",
-      "memory": "RAM",
+      "memory": "حافظه",
       "threads": "نخ‌ها",
       "xrayStatus": "Xray",
       "stopXray": "توقف",
@@ -162,11 +162,15 @@
       "historyTitleDisk": "ورودی/خروجی دیسک",
       "historyTitleOnline": "کاربران آنلاین",
       "historyTitleLoad": "میانگین بار سیستم (۱ / ۵ / ۱۵ دقیقه)",
+      "historyTitleConnections": "اتصالات فعال (TCP / UDP)",
+      "historyTitleDiskUsage": "مصرف فضای دیسک",
       "historyTabBandwidth": "پهنای باند",
       "historyTabPackets": "بسته‌ها",
-      "historyTabDisk": "Disk I/O",
+      "historyTabDisk": "دیسک I/O",
       "historyTabOnline": "آنلاین",
       "historyTabLoad": "بار",
+      "historyTabConnections": "اتصالات",
+      "historyTabDiskUsage": "مصرف دیسک",
       "charts": "نمودارها",
       "xrayMetricsTitle": "متریک‌های Xray",
       "xrayTitleHeap": "حافظه‌ی Heap تخصیص‌یافته",

+ 5 - 1
web/translation/id-ID.json

@@ -133,7 +133,7 @@
       "frequency": "Frekuensi",
       "swap": "Swap",
       "storage": "Penyimpanan",
-      "memory": "RAM",
+      "memory": "Memori",
       "threads": "Thread",
       "xrayStatus": "Xray",
       "stopXray": "Hentikan",
@@ -162,11 +162,15 @@
       "historyTitleDisk": "I/O Disk",
       "historyTitleOnline": "Klien Online",
       "historyTitleLoad": "Rata-rata Beban Sistem (1 / 5 / 15 mnt)",
+      "historyTitleConnections": "Koneksi Aktif (TCP / UDP)",
+      "historyTitleDiskUsage": "Penggunaan Ruang Disk",
       "historyTabBandwidth": "Bandwidth",
       "historyTabPackets": "Paket",
       "historyTabDisk": "Disk I/O",
       "historyTabOnline": "Online",
       "historyTabLoad": "Beban",
+      "historyTabConnections": "Koneksi",
+      "historyTabDiskUsage": "Penggunaan Disk",
       "charts": "Grafik",
       "xrayMetricsTitle": "Metrik Xray",
       "xrayTitleHeap": "Memori Heap Teralokasi",

+ 6 - 2
web/translation/ja-JP.json

@@ -131,9 +131,9 @@
       "cpu": "CPU",
       "logicalProcessors": "論理プロセッサ",
       "frequency": "周波数",
-      "swap": "Swap",
+      "swap": "スワップ",
       "storage": "ストレージ",
-      "memory": "RAM",
+      "memory": "メモリ",
       "threads": "スレッド",
       "xrayStatus": "Xray",
       "stopXray": "停止",
@@ -162,11 +162,15 @@
       "historyTitleDisk": "ディスク I/O",
       "historyTitleOnline": "オンラインクライアント",
       "historyTitleLoad": "システム平均負荷(1分 / 5分 / 15分)",
+      "historyTitleConnections": "アクティブな接続 (TCP / UDP)",
+      "historyTitleDiskUsage": "ディスク使用率",
       "historyTabBandwidth": "帯域幅",
       "historyTabPackets": "パケット",
       "historyTabDisk": "ディスク I/O",
       "historyTabOnline": "オンライン",
       "historyTabLoad": "負荷",
+      "historyTabConnections": "接続数",
+      "historyTabDiskUsage": "ディスク使用量",
       "charts": "チャート",
       "xrayMetricsTitle": "Xray メトリクス",
       "xrayTitleHeap": "割り当て済みヒープメモリ",

+ 5 - 1
web/translation/pt-BR.json

@@ -133,7 +133,7 @@
       "frequency": "Frequência",
       "swap": "Swap",
       "storage": "Armazenamento",
-      "memory": "RAM",
+      "memory": "Memória",
       "threads": "Threads",
       "xrayStatus": "Xray",
       "stopXray": "Parar",
@@ -162,11 +162,15 @@
       "historyTitleDisk": "E/S de Disco",
       "historyTitleOnline": "Clientes Online",
       "historyTitleLoad": "Média de Carga do Sistema (1 / 5 / 15 min)",
+      "historyTitleConnections": "Conexões Ativas (TCP / UDP)",
+      "historyTitleDiskUsage": "Uso do Espaço em Disco",
       "historyTabBandwidth": "Largura de Banda",
       "historyTabPackets": "Pacotes",
       "historyTabDisk": "Disco I/O",
       "historyTabOnline": "Online",
       "historyTabLoad": "Carga",
+      "historyTabConnections": "Conexões",
+      "historyTabDiskUsage": "Uso de Disco",
       "charts": "Gráficos",
       "xrayMetricsTitle": "Métricas do Xray",
       "xrayTitleHeap": "Memória Heap Alocada",

+ 7 - 3
web/translation/ru-RU.json

@@ -128,12 +128,12 @@
     },
     "index": {
       "title": "Дашборд",
-      "cpu": "CPU",
+      "cpu": "ЦП",
       "logicalProcessors": "Логические процессоры",
       "frequency": "Частота",
-      "swap": "Swap",
+      "swap": "Подкачка",
       "storage": "Диск",
-      "memory": "RAM",
+      "memory": "Память",
       "threads": "Потоки",
       "xrayStatus": "Xray",
       "stopXray": "Стоп",
@@ -162,11 +162,15 @@
       "historyTitleDisk": "Дисковый ввод-вывод",
       "historyTitleOnline": "Клиенты онлайн",
       "historyTitleLoad": "Средняя нагрузка системы (1 / 5 / 15 мин)",
+      "historyTitleConnections": "Активные соединения (TCP / UDP)",
+      "historyTitleDiskUsage": "Использование дискового пространства",
       "historyTabBandwidth": "Пропускная способность",
       "historyTabPackets": "Пакеты",
       "historyTabDisk": "Диск I/O",
       "historyTabOnline": "Онлайн",
       "historyTabLoad": "Нагрузка",
+      "historyTabConnections": "Соединения",
+      "historyTabDiskUsage": "Использование диска",
       "charts": "Графики",
       "xrayMetricsTitle": "Метрики Xray",
       "xrayTitleHeap": "Выделенная память кучи",

+ 6 - 2
web/translation/tr-TR.json

@@ -131,9 +131,9 @@
       "cpu": "CPU",
       "logicalProcessors": "Mantıksal işlemciler",
       "frequency": "Frekans",
-      "swap": "Swap",
+      "swap": "Takas",
       "storage": "Depolama",
-      "memory": "RAM",
+      "memory": "Bellek",
       "threads": "İş parçacığı",
       "xrayStatus": "Xray",
       "stopXray": "Durdur",
@@ -162,11 +162,15 @@
       "historyTitleDisk": "Disk G/Ç",
       "historyTitleOnline": "Çevrimiçi İstemciler",
       "historyTitleLoad": "Sistem Yük Ortalaması (1d / 5d / 15d)",
+      "historyTitleConnections": "Etkin Bağlantılar (TCP / UDP)",
+      "historyTitleDiskUsage": "Disk Alanı Kullanımı",
       "historyTabBandwidth": "Bant Genişliği",
       "historyTabPackets": "Paketler",
       "historyTabDisk": "Disk G/Ç",
       "historyTabOnline": "Çevrimiçi",
       "historyTabLoad": "Yük",
+      "historyTabConnections": "Bağlantılar",
+      "historyTabDiskUsage": "Disk Kullanımı",
       "charts": "Grafikler",
       "xrayMetricsTitle": "Xray Metrikleri",
       "xrayTitleHeap": "Ayrılan Yığın Belleği",

+ 7 - 3
web/translation/uk-UA.json

@@ -128,12 +128,12 @@
     },
     "index": {
       "title": "Огляд",
-      "cpu": "CPU",
+      "cpu": "ЦП",
       "logicalProcessors": "Логічні процесори",
       "frequency": "Частота",
-      "swap": "Swap",
+      "swap": "Підкачка",
       "storage": "Сховище",
-      "memory": "RAM",
+      "memory": "Пам’ять",
       "threads": "Потоки",
       "xrayStatus": "Xray",
       "stopXray": "Стоп",
@@ -162,11 +162,15 @@
       "historyTitleDisk": "Дисковий ввід-вивід",
       "historyTitleOnline": "Клієнти онлайн",
       "historyTitleLoad": "Середнє навантаження системи (1 / 5 / 15 хв)",
+      "historyTitleConnections": "Активні з’єднання (TCP / UDP)",
+      "historyTitleDiskUsage": "Використання дискового простору",
       "historyTabBandwidth": "Пропускна здатність",
       "historyTabPackets": "Пакети",
       "historyTabDisk": "Диск I/O",
       "historyTabOnline": "Онлайн",
       "historyTabLoad": "Навантаження",
+      "historyTabConnections": "З’єднання",
+      "historyTabDiskUsage": "Використання диска",
       "charts": "Графіки",
       "xrayMetricsTitle": "Метрики Xray",
       "xrayTitleHeap": "Виділена пам’ять купи",

+ 5 - 1
web/translation/vi-VN.json

@@ -133,7 +133,7 @@
       "frequency": "Tần số",
       "swap": "Swap",
       "storage": "Lưu trữ",
-      "memory": "RAM",
+      "memory": "Bộ nhớ",
       "threads": "Luồng",
       "xrayStatus": "Xray",
       "stopXray": "Dừng",
@@ -162,11 +162,15 @@
       "historyTitleDisk": "I/O đĩa",
       "historyTitleOnline": "Máy khách trực tuyến",
       "historyTitleLoad": "Tải trung bình hệ thống (1 / 5 / 15 phút)",
+      "historyTitleConnections": "Kết nối đang hoạt động (TCP / UDP)",
+      "historyTitleDiskUsage": "Sử dụng dung lượng đĩa",
       "historyTabBandwidth": "Băng thông",
       "historyTabPackets": "Gói tin",
       "historyTabDisk": "Đĩa I/O",
       "historyTabOnline": "Trực tuyến",
       "historyTabLoad": "Tải",
+      "historyTabConnections": "Kết nối",
+      "historyTabDiskUsage": "Sử dụng đĩa",
       "charts": "Biểu đồ",
       "xrayMetricsTitle": "Chỉ số Xray",
       "xrayTitleHeap": "Bộ nhớ Heap đã cấp phát",

+ 6 - 2
web/translation/zh-CN.json

@@ -131,9 +131,9 @@
       "cpu": "CPU",
       "logicalProcessors": "逻辑处理器",
       "frequency": "频率",
-      "swap": "Swap",
+      "swap": "交换空间",
       "storage": "存储",
-      "memory": "RAM",
+      "memory": "内存",
       "threads": "线程",
       "xrayStatus": "Xray",
       "stopXray": "停止",
@@ -162,11 +162,15 @@
       "historyTitleDisk": "磁盘 I/O",
       "historyTitleOnline": "在线客户端",
       "historyTitleLoad": "系统平均负载(1 分钟 / 5 分钟 / 15 分钟)",
+      "historyTitleConnections": "活动连接 (TCP / UDP)",
+      "historyTitleDiskUsage": "磁盘空间使用率",
       "historyTabBandwidth": "带宽",
       "historyTabPackets": "数据包",
       "historyTabDisk": "磁盘 I/O",
       "historyTabOnline": "在线",
       "historyTabLoad": "负载",
+      "historyTabConnections": "连接数",
+      "historyTabDiskUsage": "磁盘使用量",
       "charts": "图表",
       "xrayMetricsTitle": "Xray 指标",
       "xrayTitleHeap": "已分配的堆内存",

+ 6 - 2
web/translation/zh-TW.json

@@ -131,9 +131,9 @@
       "cpu": "CPU",
       "logicalProcessors": "邏輯處理器",
       "frequency": "頻率",
-      "swap": "Swap",
+      "swap": "交換空間",
       "storage": "儲存",
-      "memory": "RAM",
+      "memory": "記憶體",
       "threads": "執行緒",
       "xrayStatus": "Xray",
       "stopXray": "停止",
@@ -162,11 +162,15 @@
       "historyTitleDisk": "磁碟 I/O",
       "historyTitleOnline": "線上用戶端",
       "historyTitleLoad": "系統平均負載(1 分鐘 / 5 分鐘 / 15 分鐘)",
+      "historyTitleConnections": "使用中的連線 (TCP / UDP)",
+      "historyTitleDiskUsage": "磁碟空間使用率",
       "historyTabBandwidth": "頻寬",
       "historyTabPackets": "封包",
       "historyTabDisk": "磁碟 I/O",
       "historyTabOnline": "線上",
       "historyTabLoad": "負載",
+      "historyTabConnections": "連線數",
+      "historyTabDiskUsage": "磁碟使用量",
       "charts": "圖表",
       "xrayMetricsTitle": "Xray 指標",
       "xrayTitleHeap": "已配置的堆積記憶體",

+ 3 - 0
web/web.go

@@ -469,6 +469,9 @@ func (s *Server) stop(stopXray bool, stopTgBot bool) error {
 	if s.cron != nil {
 		s.cron.Stop()
 	}
+	if err := service.PersistSystemMetrics(); err != nil {
+		logger.Warning("persist system metrics on shutdown failed:", err)
+	}
 	if stopXray {
 		service.StopTrafficWriter()
 	}