mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-10 13:21:00 +00:00
feat(update): add rolling dev update channel for per-commit builds
Adds an opt-in Dev channel so panels running CI per-commit builds can self-update to the latest commit, mirroring the stable online-update flow. CI publishes/overwrites a single fixed-tag pre-release (dev-latest), force-moved to the newest main commit and marked --latest=false so releases/latest stays the stable tag. Builds stamp the short commit via -ldflags; the panel compares the running commit to the dev release commit to detect an update, and update.sh honors XUI_UPDATE_TAG to install from that tag. Linux/systemd only.
This commit is contained in:
@@ -400,6 +400,12 @@ export const sections: readonly Section[] = [
|
||||
path: '/panel/api/server/updatePanel',
|
||||
summary: 'Self-update the panel to the latest version. The server restarts on success.',
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/panel/api/server/setUpdateChannel',
|
||||
summary: 'Toggle the panel update channel between stable and the rolling per-commit dev release. Only effective on dev builds.',
|
||||
body: '{\n "dev": true\n}',
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/panel/api/server/updateGeofile',
|
||||
|
||||
@@ -65,6 +65,8 @@ export default function IndexPage() {
|
||||
useEffect(() => { setMessageInstance(messageApi); }, [messageApi]);
|
||||
|
||||
const [accessLogEnable, setAccessLogEnable] = useState(false);
|
||||
const [isDevBuild, setIsDevBuild] = useState(false);
|
||||
const [devChannelEnable, setDevChannelEnable] = useState(false);
|
||||
const [panelUpdateInfo, setPanelUpdateInfo] = useState<PanelUpdateInfo>({
|
||||
currentVersion: '',
|
||||
latestVersion: '',
|
||||
@@ -87,8 +89,14 @@ export default function IndexPage() {
|
||||
const [loadingTip, setLoadingTip] = useState(t('loading'));
|
||||
|
||||
useEffect(() => {
|
||||
HttpUtil.post<{ accessLogEnable?: boolean }>('/panel/api/setting/defaultSettings').then((msg) => {
|
||||
if (msg?.success && msg.obj) setAccessLogEnable(!!msg.obj.accessLogEnable);
|
||||
HttpUtil.post<{ accessLogEnable?: boolean; isDevBuild?: boolean; devChannelEnable?: boolean }>(
|
||||
'/panel/api/setting/defaultSettings',
|
||||
).then((msg) => {
|
||||
if (msg?.success && msg.obj) {
|
||||
setAccessLogEnable(!!msg.obj.accessLogEnable);
|
||||
setIsDevBuild(!!msg.obj.isDevBuild);
|
||||
setDevChannelEnable(!!msg.obj.devChannelEnable);
|
||||
}
|
||||
});
|
||||
HttpUtil.get<PanelUpdateInfo>('/panel/api/server/getPanelUpdateInfo').then((msg) => {
|
||||
if (msg?.success && msg.obj) setPanelUpdateInfo(msg.obj);
|
||||
@@ -119,13 +127,21 @@ export default function IndexPage() {
|
||||
}, [refresh]);
|
||||
|
||||
function openPanelVersion() {
|
||||
if (panelUpdateInfo.updateAvailable) {
|
||||
if (panelUpdateInfo.updateAvailable || isDevBuild) {
|
||||
setPanelUpdateOpen(true);
|
||||
} else {
|
||||
window.open('https://github.com/MHSanaei/3x-ui/releases', '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleChannelChange(dev: boolean) {
|
||||
const res = await HttpUtil.post('/panel/api/server/setUpdateChannel', { dev });
|
||||
if (!res?.success) return;
|
||||
setDevChannelEnable(dev);
|
||||
const msg = await HttpUtil.get<PanelUpdateInfo>('/panel/api/server/getPanelUpdateInfo');
|
||||
if (msg?.success && msg.obj) setPanelUpdateInfo(msg.obj);
|
||||
}
|
||||
|
||||
function openTelegram() {
|
||||
window.open('https://t.me/XrayUI', '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
@@ -224,7 +240,9 @@ export default function IndexPage() {
|
||||
{isMobile && displayVersion && (
|
||||
<Tag color={panelUpdateInfo.updateAvailable ? 'orange' : 'green'}>
|
||||
{panelUpdateInfo.updateAvailable
|
||||
? `v${panelUpdateInfo.latestVersion}`
|
||||
? panelUpdateInfo.channel === 'dev'
|
||||
? panelUpdateInfo.latestVersion
|
||||
: `v${panelUpdateInfo.latestVersion}`
|
||||
: `v${displayVersion}`}
|
||||
</Tag>
|
||||
)}
|
||||
@@ -446,6 +464,9 @@ export default function IndexPage() {
|
||||
<PanelUpdateModal
|
||||
open={panelUpdateOpen}
|
||||
info={panelUpdateInfo}
|
||||
isDevBuild={isDevBuild}
|
||||
devChannelEnable={devChannelEnable}
|
||||
onChannelChange={handleChannelChange}
|
||||
onClose={() => setPanelUpdateOpen(false)}
|
||||
onBusy={setBusy}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Alert, Button, Modal, Tag } from 'antd';
|
||||
import { Alert, Button, Modal, Switch, Tag } from 'antd';
|
||||
import { CloudDownloadOutlined } from '@ant-design/icons';
|
||||
import axios from 'axios';
|
||||
|
||||
@@ -7,8 +8,11 @@ import { HttpUtil, PromiseUtil } from '@/utils';
|
||||
import './PanelUpdateModal.css';
|
||||
|
||||
export interface PanelUpdateInfo {
|
||||
channel?: string;
|
||||
currentVersion: string;
|
||||
latestVersion: string;
|
||||
currentCommit?: string;
|
||||
latestCommit?: string;
|
||||
updateAvailable: boolean;
|
||||
}
|
||||
|
||||
@@ -20,13 +24,27 @@ interface BusyEvent {
|
||||
interface PanelUpdateModalProps {
|
||||
open: boolean;
|
||||
info: PanelUpdateInfo;
|
||||
isDevBuild?: boolean;
|
||||
devChannelEnable?: boolean;
|
||||
onChannelChange?: (dev: boolean) => void | Promise<void>;
|
||||
onClose: () => void;
|
||||
onBusy: (e: BusyEvent) => void;
|
||||
}
|
||||
|
||||
export default function PanelUpdateModal({ open, info, onClose, onBusy }: PanelUpdateModalProps) {
|
||||
export default function PanelUpdateModal({
|
||||
open,
|
||||
info,
|
||||
isDevBuild,
|
||||
devChannelEnable,
|
||||
onChannelChange,
|
||||
onClose,
|
||||
onBusy,
|
||||
}: PanelUpdateModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
const [channelBusy, setChannelBusy] = useState(false);
|
||||
|
||||
const isDev = info.channel === 'dev';
|
||||
|
||||
async function pollUntilBack(): Promise<boolean> {
|
||||
await PromiseUtil.sleep(5000);
|
||||
@@ -43,6 +61,16 @@ export default function PanelUpdateModal({ open, info, onClose, onBusy }: PanelU
|
||||
return false;
|
||||
}
|
||||
|
||||
async function handleChannel(checked: boolean) {
|
||||
if (!onChannelChange) return;
|
||||
setChannelBusy(true);
|
||||
try {
|
||||
await onChannelChange(checked);
|
||||
} finally {
|
||||
setChannelBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
function updatePanel() {
|
||||
modal.confirm({
|
||||
title: t('pages.index.panelUpdateDialog'),
|
||||
@@ -84,15 +112,41 @@ export default function PanelUpdateModal({ open, info, onClose, onBusy }: PanelU
|
||||
/>
|
||||
)}
|
||||
|
||||
{isDevBuild && (
|
||||
<div className="version-list">
|
||||
<div className="version-list-item">
|
||||
<span>{t('pages.index.devChannel')}</span>
|
||||
<Switch
|
||||
checked={!!devChannelEnable}
|
||||
loading={channelBusy}
|
||||
onChange={handleChannel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{devChannelEnable && (
|
||||
<Alert
|
||||
type="info"
|
||||
className="mb-12"
|
||||
title={t('pages.index.devChannelWarning')}
|
||||
showIcon
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="version-list">
|
||||
<div className="version-list-item">
|
||||
<span>{t('pages.index.currentPanelVersion')}</span>
|
||||
<Tag color="green">v{info.currentVersion || '?'}</Tag>
|
||||
<span>{isDev ? t('pages.index.currentCommit') : t('pages.index.currentPanelVersion')}</span>
|
||||
{isDev ? (
|
||||
<Tag color="green">{info.currentCommit || '?'}</Tag>
|
||||
) : (
|
||||
<Tag color="green">v{info.currentVersion || '?'}</Tag>
|
||||
)}
|
||||
</div>
|
||||
{info.updateAvailable ? (
|
||||
<div className="version-list-item">
|
||||
<span>{t('pages.index.latestPanelVersion')}</span>
|
||||
<Tag color="purple">{info.latestVersion || '-'}</Tag>
|
||||
<span>{isDev ? t('pages.index.latestCommit') : t('pages.index.latestPanelVersion')}</span>
|
||||
<Tag color="purple">{(isDev ? info.latestCommit : info.latestVersion) || '-'}</Tag>
|
||||
</div>
|
||||
) : (
|
||||
<div className="version-list-item">
|
||||
|
||||
Reference in New Issue
Block a user