From 9feeccffc02d7daf518a4d654480dd81b319c1ad Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Tue, 12 May 2026 00:27:49 +0200 Subject: [PATCH 1/5] fix(node): normalize base path during probe so missing trailing slash doesn't break status checks --- web/service/node.go | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/web/service/node.go b/web/service/node.go index 9cdaf2b7..5ed76d69 100644 --- a/web/service/node.go +++ b/web/service/node.go @@ -53,6 +53,20 @@ func (s *NodeService) GetById(id int) (*model.Node, error) { return n, nil } +func normalizeBasePath(p string) string { + p = strings.TrimSpace(p) + if p == "" { + return "/" + } + if !strings.HasPrefix(p, "/") { + p = "/" + p + } + if !strings.HasSuffix(p, "/") { + p = p + "/" + } + return p +} + func (s *NodeService) normalize(n *model.Node) error { n.Name = strings.TrimSpace(n.Name) n.Address = strings.TrimSpace(n.Address) @@ -69,15 +83,7 @@ func (s *NodeService) normalize(n *model.Node) error { if n.Scheme != "http" && n.Scheme != "https" { n.Scheme = "https" } - if n.BasePath == "" { - n.BasePath = "/" - } - if !strings.HasPrefix(n.BasePath, "/") { - n.BasePath = "/" + n.BasePath - } - if !strings.HasSuffix(n.BasePath, "/") { - n.BasePath = n.BasePath + "/" - } + n.BasePath = normalizeBasePath(n.BasePath) return nil } @@ -169,7 +175,7 @@ func (s *NodeService) AggregateNodeMetric(id int, metric string, bucketSeconds i func (s *NodeService) Probe(ctx context.Context, n *model.Node) (HeartbeatPatch, error) { patch := HeartbeatPatch{LastHeartbeat: time.Now().Unix()} url := fmt.Sprintf("%s://%s:%d%spanel/api/server/status", - n.Scheme, n.Address, n.Port, n.BasePath) + n.Scheme, n.Address, n.Port, normalizeBasePath(n.BasePath)) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { From 355bb4c9c001d252845b4f8bf3ae4225c2cd460e Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Tue, 12 May 2026 02:17:45 +0200 Subject: [PATCH 2/5] feat(panel): xray metrics dashboard with observatory probe history Polls xray's /debug/vars on the 2s status tick, stores memstats and per-outbound observatory delay in the metric history ring buffer, and exposes them through a new XrayMetricsModal opened from the Charts card. Restructures the dashboard to consolidate uptime, usage, version, and Telegram link into stat-style or action-style cards consistent with the existing AntD aesthetic. --- frontend/src/pages/index/IndexPage.vue | 158 ++++---- frontend/src/pages/index/XrayMetricsModal.vue | 347 ++++++++++++++++++ web/controller/server.go | 51 ++- web/service/metric_history.go | 9 + web/service/xray_metrics.go | 224 +++++++++++ web/translation/ar-EG.json | 11 + web/translation/en-US.json | 11 + web/translation/es-ES.json | 11 + web/translation/fa-IR.json | 11 + web/translation/id-ID.json | 11 + web/translation/ja-JP.json | 11 + web/translation/pt-BR.json | 11 + web/translation/ru-RU.json | 11 + web/translation/tr-TR.json | 11 + web/translation/uk-UA.json | 11 + web/translation/vi-VN.json | 11 + web/translation/zh-CN.json | 11 + web/translation/zh-TW.json | 11 + 18 files changed, 865 insertions(+), 67 deletions(-) create mode 100644 frontend/src/pages/index/XrayMetricsModal.vue create mode 100644 web/service/xray_metrics.go diff --git a/frontend/src/pages/index/IndexPage.vue b/frontend/src/pages/index/IndexPage.vue index 292e4156..0551c2b6 100644 --- a/frontend/src/pages/index/IndexPage.vue +++ b/frontend/src/pages/index/IndexPage.vue @@ -14,6 +14,10 @@ import { SwapOutlined, EyeOutlined, EyeInvisibleOutlined, + ThunderboltOutlined, + DesktopOutlined, + DatabaseOutlined, + ForkOutlined, } from '@ant-design/icons-vue'; const { t } = useI18n(); @@ -31,6 +35,7 @@ import PanelUpdateModal from './PanelUpdateModal.vue'; import LogModal from './LogModal.vue'; import BackupModal from './BackupModal.vue'; import SystemHistoryModal from './SystemHistoryModal.vue'; +import XrayMetricsModal from './XrayMetricsModal.vue'; import XrayLogModal from './XrayLogModal.vue'; import VersionModal from './VersionModal.vue'; @@ -71,6 +76,7 @@ const logsOpen = ref(false); const backupOpen = ref(false); const panelUpdateOpen = ref(false); const sysHistoryOpen = ref(false); +const xrayMetricsOpen = ref(false); const xrayLogsOpen = ref(false); const versionOpen = ref(false); const configTextOpen = ref(false); @@ -98,6 +104,18 @@ function openSystemHistory() { sysHistoryOpen.value = true; } function openXrayLogs() { xrayLogsOpen.value = true; } function openVersionSwitch() { versionOpen.value = true; } +function openPanelVersion() { + if (panelUpdateInfo.value.updateAvailable) { + panelUpdateOpen.value = true; + } else { + window.open('https://github.com/MHSanaei/3x-ui/releases', '_blank', 'noopener,noreferrer'); + } +} + +function openTelegram() { + window.open('https://t.me/XrayUI', '_blank', 'noopener,noreferrer'); +} + // Legacy "Config" action — fetch the rendered xray config and show // it as JSON in the shared TextModal (same UX as main). async function openConfig() { @@ -155,62 +173,83 @@ async function openConfig() { -