From e4b881e58a8ac99804c8a4261df09aa7366b62f2 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Thu, 25 Jun 2026 02:36:41 +0200 Subject: [PATCH] feat(panel): surface dev-build version in UI, bot, and CLI A dev build now shows its `dev+` identity instead of a misleading stable-looking version in the sidebar badge, dashboard card, update modal, Telegram status report, startup log, and `x-ui -v`. Adds a shared formatPanelVersion helper (single v prefix; dev labels shown verbatim) and fixes the mobile-tag double-v. Renames the version getters for clarity: config.GetVersion to GetBaseVersion (raw embedded version), config.GetReportedVersion to GetPanelVersion (advertised/displayed), and the xray process GetVersion to GetXrayVersion. --- frontend/src/layouts/AppSidebar.tsx | 3 ++- frontend/src/lib/panel-version.ts | 12 +++++++++ frontend/src/pages/index/IndexPage.tsx | 13 +++++----- frontend/src/pages/index/PanelUpdateModal.tsx | 3 ++- frontend/src/test/panel-version.test.ts | 25 ++++++++++++++++++- internal/config/config.go | 21 +++++++++------- internal/config/config_test.go | 14 +++++------ internal/web/controller/dist.go | 2 +- internal/web/service/panel/panel.go | 4 +-- internal/web/service/server.go | 2 +- internal/web/service/tgbot/tgbot_report.go | 2 +- internal/web/service/xray.go | 2 +- internal/xray/process.go | 4 +-- main.go | 4 +-- 14 files changed, 75 insertions(+), 36 deletions(-) diff --git a/frontend/src/layouts/AppSidebar.tsx b/frontend/src/layouts/AppSidebar.tsx index df0f07c6c..22e747adf 100644 --- a/frontend/src/layouts/AppSidebar.tsx +++ b/frontend/src/layouts/AppSidebar.tsx @@ -33,6 +33,7 @@ import { } from '@ant-design/icons'; import { HttpUtil } from '@/utils'; +import { formatPanelVersion } from '@/lib/panel-version'; import { pauseAnimationsUntilLeave, useTheme } from '@/hooks/useTheme'; import { useAllSettings } from '@/api/queries/useAllSettings'; import './AppSidebar.css'; @@ -84,7 +85,7 @@ function DonateButton({ ariaLabel }: { ariaLabel: string }) { function VersionBadge({ version, collapsed }: { version: string; collapsed?: boolean }) { if (!version) return null; - const label = `v${version}`; + const label = formatPanelVersion(version); return ( " +// identity (see config.GetPanelVersion); show those — and any other +// non-numeric label — verbatim. Semantic versions get a single normalized "v" +// prefix, so a raw "v3.4.0" tag and a bare "3.4.0" both render as "v3.4.0" +// instead of doubling up to "vv3.4.0". +export function formatPanelVersion(version: string | undefined | null): string { + const v = (version || '').trim(); + if (!v) return ''; + const normalized = v.replace(/^v/i, ''); + return /^\d/.test(normalized) ? `v${normalized}` : v; +} + export function isPanelUpdateAvailable(latest: string, current: string): boolean { if (!latest || !current) return false; const a = parseVersionParts(latest); diff --git a/frontend/src/pages/index/IndexPage.tsx b/frontend/src/pages/index/IndexPage.tsx index 496bba24a..1a22a321a 100644 --- a/frontend/src/pages/index/IndexPage.tsx +++ b/frontend/src/pages/index/IndexPage.tsx @@ -37,6 +37,7 @@ import { } from '@ant-design/icons'; import { HttpUtil, SizeFormatter, TimeFormatter, ClipboardManager, FileManager } from '@/utils'; +import { formatPanelVersion } from '@/lib/panel-version'; import { useTheme } from '@/hooks/useTheme'; import { useStatusQuery } from '@/api/queries/useStatusQuery'; import { useMediaQuery } from '@/hooks/useMediaQuery'; @@ -104,7 +105,7 @@ export default function IndexPage() { }, []); const displayVersion = useMemo( - () => panelUpdateInfo.currentVersion || window.X_UI_CUR_VER || '?', + () => window.X_UI_CUR_VER || panelUpdateInfo.currentVersion || '?', [panelUpdateInfo.currentVersion], ); @@ -240,10 +241,8 @@ export default function IndexPage() { {isMobile && displayVersion && ( {panelUpdateInfo.updateAvailable - ? panelUpdateInfo.channel === 'dev' - ? panelUpdateInfo.latestVersion - : `v${panelUpdateInfo.latestVersion}` - : `v${displayVersion}`} + ? formatPanelVersion(panelUpdateInfo.latestVersion) + : formatPanelVersion(displayVersion)} )} @@ -272,8 +271,8 @@ export default function IndexPage() { {!isMobile && ( {panelUpdateInfo.updateAvailable - ? `${t('update')} ${panelUpdateInfo.latestVersion}` - : `v${displayVersion}`} + ? `${t('update')} ${formatPanelVersion(panelUpdateInfo.latestVersion)}` + : formatPanelVersion(displayVersion)} )} , diff --git a/frontend/src/pages/index/PanelUpdateModal.tsx b/frontend/src/pages/index/PanelUpdateModal.tsx index 1268ee2d8..a6000b0f5 100644 --- a/frontend/src/pages/index/PanelUpdateModal.tsx +++ b/frontend/src/pages/index/PanelUpdateModal.tsx @@ -5,6 +5,7 @@ import { CloudDownloadOutlined } from '@ant-design/icons'; import axios from 'axios'; import { HttpUtil, PromiseUtil } from '@/utils'; +import { formatPanelVersion } from '@/lib/panel-version'; import './PanelUpdateModal.css'; export interface PanelUpdateInfo { @@ -140,7 +141,7 @@ export default function PanelUpdateModal({ {isDev ? ( {info.currentCommit || '?'} ) : ( - v{info.currentVersion || '?'} + {formatPanelVersion(window.X_UI_CUR_VER || info.currentVersion) || '?'} )} {info.updateAvailable ? ( diff --git a/frontend/src/test/panel-version.test.ts b/frontend/src/test/panel-version.test.ts index cd29db062..5f628968f 100644 --- a/frontend/src/test/panel-version.test.ts +++ b/frontend/src/test/panel-version.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; -import { isPanelUpdateAvailable } from '@/lib/panel-version'; +import { formatPanelVersion, isPanelUpdateAvailable } from '@/lib/panel-version'; // Parity with web/service/panel.go isNewerVersion. describe('isPanelUpdateAvailable', () => { @@ -31,3 +31,26 @@ describe('isPanelUpdateAvailable', () => { expect(isPanelUpdateAvailable('nightly-1', 'nightly-1')).toBe(false); }); }); + +describe('formatPanelVersion', () => { + it('adds a single v prefix to bare semantic versions', () => { + expect(formatPanelVersion('3.4.0')).toBe('v3.4.0'); + expect(formatPanelVersion('2.6.5')).toBe('v2.6.5'); + }); + + it('does not double up the v on already-prefixed tags', () => { + expect(formatPanelVersion('v3.4.0')).toBe('v3.4.0'); + expect(formatPanelVersion('V3.4.0')).toBe('v3.4.0'); + }); + + it('shows dev builds verbatim without a v prefix', () => { + expect(formatPanelVersion('dev+1a2b3c4d')).toBe('dev+1a2b3c4d'); + expect(formatPanelVersion('dev')).toBe('dev'); + }); + + it('returns empty for blank input and leaves unknown markers untouched', () => { + expect(formatPanelVersion('')).toBe(''); + expect(formatPanelVersion(undefined)).toBe(''); + expect(formatPanelVersion('?')).toBe('?'); + }); +}); diff --git a/internal/config/config.go b/internal/config/config.go index 14602830a..080fc4b4f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -41,8 +41,11 @@ const ( Error LogLevel = "error" ) -// GetVersion returns the version string of the 3x-ui application. -func GetVersion() string { +// GetBaseVersion returns the raw embedded release version of the 3x-ui panel +// (e.g. "3.4.0"). This is the panel's own version, not the Xray version. For the +// version a panel advertises/displays (which adds a "dev+" label on dev +// builds), use GetPanelVersion. +func GetBaseVersion() string { return strings.TrimSpace(version) } @@ -68,14 +71,14 @@ func IsDevBuild() bool { return GetBuildCommit() != "" } -// GetReportedVersion returns the version a panel advertises to a managing master -// node: the plain version for stable builds, or "dev+" for dev -// builds. The dev form mirrors the master's getPanelUpdateInfo latestVersion so -// a node on the current dev commit compares as up to date instead of always -// showing "update available". -func GetReportedVersion() string { +// GetPanelVersion returns the version a panel advertises to a managing master +// node and displays in the UI: the plain version for stable builds, or +// "dev+" for dev builds. The dev form mirrors the master's +// getPanelUpdateInfo latestVersion so a node on the current dev commit compares +// as up to date instead of always showing "update available". +func GetPanelVersion() string { if !IsDevBuild() { - return GetVersion() + return GetBaseVersion() } commit := GetBuildCommit() if len(commit) > 8 { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 0f1511245..bcfcd995e 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -5,23 +5,23 @@ import ( "testing" ) -func TestGetReportedVersion(t *testing.T) { +func TestGetPanelVersion(t *testing.T) { orig := buildCommit t.Cleanup(func() { buildCommit = orig }) buildCommit = "" - if got := GetReportedVersion(); got != GetVersion() { - t.Fatalf("stable build: GetReportedVersion = %q, want %q", got, GetVersion()) + if got := GetPanelVersion(); got != GetBaseVersion() { + t.Fatalf("stable build: GetPanelVersion = %q, want %q", got, GetBaseVersion()) } buildCommit = "1d1128cf" - if got := GetReportedVersion(); got != "dev+1d1128cf" { - t.Fatalf("dev build: GetReportedVersion = %q, want %q", got, "dev+1d1128cf") + if got := GetPanelVersion(); got != "dev+1d1128cf" { + t.Fatalf("dev build: GetPanelVersion = %q, want %q", got, "dev+1d1128cf") } buildCommit = "1d1128cf945c4615efa05cf41ba7fa766e2ee428" - if got := GetReportedVersion(); got != "dev+1d1128cf" { - t.Fatalf("dev build (full sha): GetReportedVersion = %q, want %q", got, "dev+1d1128cf") + if got := GetPanelVersion(); got != "dev+1d1128cf" { + t.Fatalf("dev build (full sha): GetPanelVersion = %q, want %q", got, "dev+1d1128cf") } } diff --git a/internal/web/controller/dist.go b/internal/web/controller/dist.go index b0d6eedb1..996ca59d6 100644 --- a/internal/web/controller/dist.go +++ b/internal/web/controller/dist.go @@ -112,7 +112,7 @@ func serveDistPage(c *gin.Context, name string) { } script := `window.X_UI_BASE_PATH="` + escapedBase + `"` if name != "login.html" { - escapedVer := jsEscape.Replace(config.GetVersion()) + escapedVer := jsEscape.Replace(config.GetPanelVersion()) script += `;window.X_UI_CUR_VER="` + escapedVer + `"` script += `;window.X_UI_DB_TYPE="` + config.GetDBKind() + `"` } diff --git a/internal/web/service/panel/panel.go b/internal/web/service/panel/panel.go index 23bb38657..0a1edd9bc 100644 --- a/internal/web/service/panel/panel.go +++ b/internal/web/service/panel/panel.go @@ -80,7 +80,7 @@ func (s *PanelService) GetUpdateInfo() (*PanelUpdateInfo, error) { if err != nil { return nil, err } - current := config.GetVersion() + current := config.GetBaseVersion() return &PanelUpdateInfo{ Channel: "stable", CurrentVersion: current, @@ -114,7 +114,7 @@ func getDevUpdateInfo() (*PanelUpdateInfo, error) { currentCommit := config.GetBuildCommit() return &PanelUpdateInfo{ Channel: "dev", - CurrentVersion: config.GetVersion(), + CurrentVersion: config.GetPanelVersion(), CurrentCommit: shortCommit(currentCommit), LatestCommit: shortCommit(latestCommit), LatestVersion: "dev+" + shortCommit(latestCommit), diff --git a/internal/web/service/server.go b/internal/web/service/server.go index b09d5cc50..60d83bd04 100644 --- a/internal/web/service/server.go +++ b/internal/web/service/server.go @@ -604,7 +604,7 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status { status.Xray.ErrorMsg = s.xrayService.GetXrayResult() } status.Xray.Version = s.xrayService.GetXrayVersion() - status.PanelVersion = config.GetReportedVersion() + status.PanelVersion = config.GetPanelVersion() if guid, err := s.settingService.GetPanelGuid(); err == nil { status.PanelGuid = guid } diff --git a/internal/web/service/tgbot/tgbot_report.go b/internal/web/service/tgbot/tgbot_report.go index d6942404d..39214e9ac 100644 --- a/internal/web/service/tgbot/tgbot_report.go +++ b/internal/web/service/tgbot/tgbot_report.go @@ -108,7 +108,7 @@ func (t *Tgbot) prepareServerUsageInfo() string { onlines := service.XrayProcess().GetOnlineClients() info += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname) - info += t.I18nBot("tgbot.messages.version", "Version=="+config.GetVersion()) + info += t.I18nBot("tgbot.messages.version", "Version=="+config.GetPanelVersion()) info += t.I18nBot("tgbot.messages.xrayVersion", "XrayVersion=="+fmt.Sprint(t.lastStatus.Xray.Version)) // get ip address diff --git a/internal/web/service/xray.go b/internal/web/service/xray.go index 741f49373..3c3d710b1 100644 --- a/internal/web/service/xray.go +++ b/internal/web/service/xray.go @@ -96,7 +96,7 @@ func (s *XrayService) GetXrayVersion() string { if p == nil { return "Unknown" } - return p.GetVersion() + return p.GetXrayVersion() } // RemoveIndex removes an element at the specified index from a slice. diff --git a/internal/xray/process.go b/internal/xray/process.go index ca8230241..4e6342709 100644 --- a/internal/xray/process.go +++ b/internal/xray/process.go @@ -270,8 +270,8 @@ func (p *process) GetResult() string { return lastLine } -// GetVersion returns the version string of the Xray process. -func (p *process) GetVersion() string { +// GetXrayVersion returns the version string of the Xray process. +func (p *process) GetXrayVersion() string { return p.version } diff --git a/main.go b/main.go index f7be41d74..b4dcaa642 100644 --- a/main.go +++ b/main.go @@ -34,7 +34,7 @@ import ( // runWebServer initializes and starts the web server for the 3x-ui panel. func runWebServer() { - log.Printf("Starting %v %v", config.GetName(), config.GetVersion()) + log.Printf("Starting %v %v", config.GetName(), config.GetPanelVersion()) switch config.GetLogLevel() { case config.Debug: @@ -587,7 +587,7 @@ func main() { flag.Parse() if showVersion { - fmt.Println(config.GetVersion()) + fmt.Println(config.GetPanelVersion()) return }