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
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
18 changed files with 203 additions and 33 deletions

View file

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

View file

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

View file

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

View file

@ -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())

View file

@ -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": "ذاكرة الكومة المخصصة",

View file

@ -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",

View file

@ -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",

View file

@ -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 تخصیص‌یافته",

View file

@ -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",

View file

@ -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": "割り当て済みヒープメモリ",

View file

@ -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",

View file

@ -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": "Выделенная память кучи",

View file

@ -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",

View file

@ -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": "Виділена пам’ять купи",

View file

@ -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",

View file

@ -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": "已分配的堆内存",

View file

@ -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": "已配置的堆積記憶體",

View file

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