mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-15 15:50:59 +00:00
feat(panel): surface dev-build version in UI, bot, and CLI
A dev build now shows its `dev+<commit>` 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.
This commit is contained in:
@@ -33,6 +33,7 @@ import {
|
|||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
import { HttpUtil } from '@/utils';
|
import { HttpUtil } from '@/utils';
|
||||||
|
import { formatPanelVersion } from '@/lib/panel-version';
|
||||||
import { pauseAnimationsUntilLeave, useTheme } from '@/hooks/useTheme';
|
import { pauseAnimationsUntilLeave, useTheme } from '@/hooks/useTheme';
|
||||||
import { useAllSettings } from '@/api/queries/useAllSettings';
|
import { useAllSettings } from '@/api/queries/useAllSettings';
|
||||||
import './AppSidebar.css';
|
import './AppSidebar.css';
|
||||||
@@ -84,7 +85,7 @@ function DonateButton({ ariaLabel }: { ariaLabel: string }) {
|
|||||||
|
|
||||||
function VersionBadge({ version, collapsed }: { version: string; collapsed?: boolean }) {
|
function VersionBadge({ version, collapsed }: { version: string; collapsed?: boolean }) {
|
||||||
if (!version) return null;
|
if (!version) return null;
|
||||||
const label = `v${version}`;
|
const label = formatPanelVersion(version);
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={REPO_URL}
|
href={REPO_URL}
|
||||||
|
|||||||
@@ -14,6 +14,18 @@ function parseVersionParts(version: string): [number, number, number] | null {
|
|||||||
return [out[0], out[1], out[2]];
|
return [out[0], out[1], out[2]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Format a panel version for display. Dev builds report a "dev+<commit>"
|
||||||
|
// 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 {
|
export function isPanelUpdateAvailable(latest: string, current: string): boolean {
|
||||||
if (!latest || !current) return false;
|
if (!latest || !current) return false;
|
||||||
const a = parseVersionParts(latest);
|
const a = parseVersionParts(latest);
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
import { HttpUtil, SizeFormatter, TimeFormatter, ClipboardManager, FileManager } from '@/utils';
|
import { HttpUtil, SizeFormatter, TimeFormatter, ClipboardManager, FileManager } from '@/utils';
|
||||||
|
import { formatPanelVersion } from '@/lib/panel-version';
|
||||||
import { useTheme } from '@/hooks/useTheme';
|
import { useTheme } from '@/hooks/useTheme';
|
||||||
import { useStatusQuery } from '@/api/queries/useStatusQuery';
|
import { useStatusQuery } from '@/api/queries/useStatusQuery';
|
||||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||||
@@ -104,7 +105,7 @@ export default function IndexPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const displayVersion = useMemo(
|
const displayVersion = useMemo(
|
||||||
() => panelUpdateInfo.currentVersion || window.X_UI_CUR_VER || '?',
|
() => window.X_UI_CUR_VER || panelUpdateInfo.currentVersion || '?',
|
||||||
[panelUpdateInfo.currentVersion],
|
[panelUpdateInfo.currentVersion],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -240,10 +241,8 @@ export default function IndexPage() {
|
|||||||
{isMobile && displayVersion && (
|
{isMobile && displayVersion && (
|
||||||
<Tag color={panelUpdateInfo.updateAvailable ? 'orange' : 'green'}>
|
<Tag color={panelUpdateInfo.updateAvailable ? 'orange' : 'green'}>
|
||||||
{panelUpdateInfo.updateAvailable
|
{panelUpdateInfo.updateAvailable
|
||||||
? panelUpdateInfo.channel === 'dev'
|
? formatPanelVersion(panelUpdateInfo.latestVersion)
|
||||||
? panelUpdateInfo.latestVersion
|
: formatPanelVersion(displayVersion)}
|
||||||
: `v${panelUpdateInfo.latestVersion}`
|
|
||||||
: `v${displayVersion}`}
|
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
@@ -272,8 +271,8 @@ export default function IndexPage() {
|
|||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<span>
|
<span>
|
||||||
{panelUpdateInfo.updateAvailable
|
{panelUpdateInfo.updateAvailable
|
||||||
? `${t('update')} ${panelUpdateInfo.latestVersion}`
|
? `${t('update')} ${formatPanelVersion(panelUpdateInfo.latestVersion)}`
|
||||||
: `v${displayVersion}`}
|
: formatPanelVersion(displayVersion)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Space>,
|
</Space>,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { CloudDownloadOutlined } from '@ant-design/icons';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
import { HttpUtil, PromiseUtil } from '@/utils';
|
import { HttpUtil, PromiseUtil } from '@/utils';
|
||||||
|
import { formatPanelVersion } from '@/lib/panel-version';
|
||||||
import './PanelUpdateModal.css';
|
import './PanelUpdateModal.css';
|
||||||
|
|
||||||
export interface PanelUpdateInfo {
|
export interface PanelUpdateInfo {
|
||||||
@@ -140,7 +141,7 @@ export default function PanelUpdateModal({
|
|||||||
{isDev ? (
|
{isDev ? (
|
||||||
<Tag color="green">{info.currentCommit || '?'}</Tag>
|
<Tag color="green">{info.currentCommit || '?'}</Tag>
|
||||||
) : (
|
) : (
|
||||||
<Tag color="green">v{info.currentVersion || '?'}</Tag>
|
<Tag color="green">{formatPanelVersion(window.X_UI_CUR_VER || info.currentVersion) || '?'}</Tag>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{info.updateAvailable ? (
|
{info.updateAvailable ? (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
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.
|
// Parity with web/service/panel.go isNewerVersion.
|
||||||
describe('isPanelUpdateAvailable', () => {
|
describe('isPanelUpdateAvailable', () => {
|
||||||
@@ -31,3 +31,26 @@ describe('isPanelUpdateAvailable', () => {
|
|||||||
expect(isPanelUpdateAvailable('nightly-1', 'nightly-1')).toBe(false);
|
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('?');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -41,8 +41,11 @@ const (
|
|||||||
Error LogLevel = "error"
|
Error LogLevel = "error"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetVersion returns the version string of the 3x-ui application.
|
// GetBaseVersion returns the raw embedded release version of the 3x-ui panel
|
||||||
func GetVersion() string {
|
// (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+<sha>" label on dev
|
||||||
|
// builds), use GetPanelVersion.
|
||||||
|
func GetBaseVersion() string {
|
||||||
return strings.TrimSpace(version)
|
return strings.TrimSpace(version)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,14 +71,14 @@ func IsDevBuild() bool {
|
|||||||
return GetBuildCommit() != ""
|
return GetBuildCommit() != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetReportedVersion returns the version a panel advertises to a managing master
|
// GetPanelVersion returns the version a panel advertises to a managing master
|
||||||
// node: the plain version for stable builds, or "dev+<short commit>" for dev
|
// node and displays in the UI: the plain version for stable builds, or
|
||||||
// builds. The dev form mirrors the master's getPanelUpdateInfo latestVersion so
|
// "dev+<short commit>" for dev builds. The dev form mirrors the master's
|
||||||
// a node on the current dev commit compares as up to date instead of always
|
// getPanelUpdateInfo latestVersion so a node on the current dev commit compares
|
||||||
// showing "update available".
|
// as up to date instead of always showing "update available".
|
||||||
func GetReportedVersion() string {
|
func GetPanelVersion() string {
|
||||||
if !IsDevBuild() {
|
if !IsDevBuild() {
|
||||||
return GetVersion()
|
return GetBaseVersion()
|
||||||
}
|
}
|
||||||
commit := GetBuildCommit()
|
commit := GetBuildCommit()
|
||||||
if len(commit) > 8 {
|
if len(commit) > 8 {
|
||||||
|
|||||||
@@ -5,23 +5,23 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetReportedVersion(t *testing.T) {
|
func TestGetPanelVersion(t *testing.T) {
|
||||||
orig := buildCommit
|
orig := buildCommit
|
||||||
t.Cleanup(func() { buildCommit = orig })
|
t.Cleanup(func() { buildCommit = orig })
|
||||||
|
|
||||||
buildCommit = ""
|
buildCommit = ""
|
||||||
if got := GetReportedVersion(); got != GetVersion() {
|
if got := GetPanelVersion(); got != GetBaseVersion() {
|
||||||
t.Fatalf("stable build: GetReportedVersion = %q, want %q", got, GetVersion())
|
t.Fatalf("stable build: GetPanelVersion = %q, want %q", got, GetBaseVersion())
|
||||||
}
|
}
|
||||||
|
|
||||||
buildCommit = "1d1128cf"
|
buildCommit = "1d1128cf"
|
||||||
if got := GetReportedVersion(); got != "dev+1d1128cf" {
|
if got := GetPanelVersion(); got != "dev+1d1128cf" {
|
||||||
t.Fatalf("dev build: GetReportedVersion = %q, want %q", got, "dev+1d1128cf")
|
t.Fatalf("dev build: GetPanelVersion = %q, want %q", got, "dev+1d1128cf")
|
||||||
}
|
}
|
||||||
|
|
||||||
buildCommit = "1d1128cf945c4615efa05cf41ba7fa766e2ee428"
|
buildCommit = "1d1128cf945c4615efa05cf41ba7fa766e2ee428"
|
||||||
if got := GetReportedVersion(); got != "dev+1d1128cf" {
|
if got := GetPanelVersion(); got != "dev+1d1128cf" {
|
||||||
t.Fatalf("dev build (full sha): GetReportedVersion = %q, want %q", got, "dev+1d1128cf")
|
t.Fatalf("dev build (full sha): GetPanelVersion = %q, want %q", got, "dev+1d1128cf")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ func serveDistPage(c *gin.Context, name string) {
|
|||||||
}
|
}
|
||||||
script := `<script` + nonceAttr + `>window.X_UI_BASE_PATH="` + escapedBase + `"`
|
script := `<script` + nonceAttr + `>window.X_UI_BASE_PATH="` + escapedBase + `"`
|
||||||
if name != "login.html" {
|
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_CUR_VER="` + escapedVer + `"`
|
||||||
script += `;window.X_UI_DB_TYPE="` + config.GetDBKind() + `"`
|
script += `;window.X_UI_DB_TYPE="` + config.GetDBKind() + `"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ func (s *PanelService) GetUpdateInfo() (*PanelUpdateInfo, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
current := config.GetVersion()
|
current := config.GetBaseVersion()
|
||||||
return &PanelUpdateInfo{
|
return &PanelUpdateInfo{
|
||||||
Channel: "stable",
|
Channel: "stable",
|
||||||
CurrentVersion: current,
|
CurrentVersion: current,
|
||||||
@@ -114,7 +114,7 @@ func getDevUpdateInfo() (*PanelUpdateInfo, error) {
|
|||||||
currentCommit := config.GetBuildCommit()
|
currentCommit := config.GetBuildCommit()
|
||||||
return &PanelUpdateInfo{
|
return &PanelUpdateInfo{
|
||||||
Channel: "dev",
|
Channel: "dev",
|
||||||
CurrentVersion: config.GetVersion(),
|
CurrentVersion: config.GetPanelVersion(),
|
||||||
CurrentCommit: shortCommit(currentCommit),
|
CurrentCommit: shortCommit(currentCommit),
|
||||||
LatestCommit: shortCommit(latestCommit),
|
LatestCommit: shortCommit(latestCommit),
|
||||||
LatestVersion: "dev+" + shortCommit(latestCommit),
|
LatestVersion: "dev+" + shortCommit(latestCommit),
|
||||||
|
|||||||
@@ -604,7 +604,7 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
|
|||||||
status.Xray.ErrorMsg = s.xrayService.GetXrayResult()
|
status.Xray.ErrorMsg = s.xrayService.GetXrayResult()
|
||||||
}
|
}
|
||||||
status.Xray.Version = s.xrayService.GetXrayVersion()
|
status.Xray.Version = s.xrayService.GetXrayVersion()
|
||||||
status.PanelVersion = config.GetReportedVersion()
|
status.PanelVersion = config.GetPanelVersion()
|
||||||
if guid, err := s.settingService.GetPanelGuid(); err == nil {
|
if guid, err := s.settingService.GetPanelGuid(); err == nil {
|
||||||
status.PanelGuid = guid
|
status.PanelGuid = guid
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ func (t *Tgbot) prepareServerUsageInfo() string {
|
|||||||
onlines := service.XrayProcess().GetOnlineClients()
|
onlines := service.XrayProcess().GetOnlineClients()
|
||||||
|
|
||||||
info += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname)
|
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))
|
info += t.I18nBot("tgbot.messages.xrayVersion", "XrayVersion=="+fmt.Sprint(t.lastStatus.Xray.Version))
|
||||||
|
|
||||||
// get ip address
|
// get ip address
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ func (s *XrayService) GetXrayVersion() string {
|
|||||||
if p == nil {
|
if p == nil {
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
}
|
}
|
||||||
return p.GetVersion()
|
return p.GetXrayVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveIndex removes an element at the specified index from a slice.
|
// RemoveIndex removes an element at the specified index from a slice.
|
||||||
|
|||||||
@@ -270,8 +270,8 @@ func (p *process) GetResult() string {
|
|||||||
return lastLine
|
return lastLine
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetVersion returns the version string of the Xray process.
|
// GetXrayVersion returns the version string of the Xray process.
|
||||||
func (p *process) GetVersion() string {
|
func (p *process) GetXrayVersion() string {
|
||||||
return p.version
|
return p.version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import (
|
|||||||
|
|
||||||
// runWebServer initializes and starts the web server for the 3x-ui panel.
|
// runWebServer initializes and starts the web server for the 3x-ui panel.
|
||||||
func runWebServer() {
|
func runWebServer() {
|
||||||
log.Printf("Starting %v %v", config.GetName(), config.GetVersion())
|
log.Printf("Starting %v %v", config.GetName(), config.GetPanelVersion())
|
||||||
|
|
||||||
switch config.GetLogLevel() {
|
switch config.GetLogLevel() {
|
||||||
case config.Debug:
|
case config.Debug:
|
||||||
@@ -587,7 +587,7 @@ func main() {
|
|||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
if showVersion {
|
if showVersion {
|
||||||
fmt.Println(config.GetVersion())
|
fmt.Println(config.GetPanelVersion())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user