mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-27 19:21:34 +00:00
feat(nodes): add Dev channel option to node panel updates
The node update confirm dialog now offers a 'Dev channel (latest commit)' choice. The dev flag threads master -> nodes/updatePanel -> UpdatePanels -> remote.UpdatePanel -> the node's updatePanel endpoint, which calls StartUpdateChannel(dev) to install the rolling dev-latest build. With no dev flag the node keeps following its own channel setting.
This commit is contained in:
parent
11c5b53fac
commit
e8878b71a4
22 changed files with 100 additions and 25 deletions
|
|
@ -7440,7 +7440,7 @@
|
|||
"tags": [
|
||||
"Nodes"
|
||||
],
|
||||
"summary": "Trigger the official panel self-updater on each given node (downloads the latest release and restarts). Only enabled, online nodes are updated; offline/disabled ones are reported as skipped. Returns a per-node result list.",
|
||||
"summary": "Trigger the official panel self-updater on each given node (downloads the latest release and restarts). Only enabled, online nodes are updated; offline/disabled ones are reported as skipped. Set \"dev\": true to move the nodes to the rolling per-commit dev channel instead of the latest stable release. Returns a per-node result list.",
|
||||
"operationId": "post_panel_api_nodes_updatePanel",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
|
|
@ -7454,7 +7454,8 @@
|
|||
1,
|
||||
2,
|
||||
3
|
||||
]
|
||||
],
|
||||
"dev": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,8 +59,8 @@ export function useNodeMutations() {
|
|||
});
|
||||
|
||||
const updatePanelsMut = useMutation({
|
||||
mutationFn: (ids: number[]) =>
|
||||
HttpUtil.post<NodeUpdateResult[]>('/panel/api/nodes/updatePanel', { ids }, {
|
||||
mutationFn: ({ ids, dev }: { ids: number[]; dev: boolean }) =>
|
||||
HttpUtil.post<NodeUpdateResult[]>('/panel/api/nodes/updatePanel', { ids, dev }, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
||||
|
|
@ -72,7 +72,7 @@ export function useNodeMutations() {
|
|||
remove: (id: number) => removeMut.mutateAsync(id),
|
||||
setEnable: (id: number, enable: boolean) => setEnableMut.mutateAsync({ id, enable }),
|
||||
probe: (id: number) => probeMut.mutateAsync(id),
|
||||
updatePanels: (ids: number[]): Promise<Msg<NodeUpdateResult[]>> => updatePanelsMut.mutateAsync(ids),
|
||||
updatePanels: (ids: number[], dev: boolean): Promise<Msg<NodeUpdateResult[]>> => updatePanelsMut.mutateAsync({ ids, dev }),
|
||||
testConnection: async (payload: Partial<NodeRecord>): Promise<Msg<ProbeResult>> => {
|
||||
const raw = await HttpUtil.post('/panel/api/nodes/test', payload);
|
||||
return parseMsg(raw, ProbeResultSchema, 'nodes/test');
|
||||
|
|
|
|||
|
|
@ -945,8 +945,8 @@ export const sections: readonly Section[] = [
|
|||
{
|
||||
method: 'POST',
|
||||
path: '/panel/api/nodes/updatePanel',
|
||||
summary: 'Trigger the official panel self-updater on each given node (downloads the latest release and restarts). Only enabled, online nodes are updated; offline/disabled ones are reported as skipped. Returns a per-node result list.',
|
||||
body: '{\n "ids": [1, 2, 3]\n}',
|
||||
summary: 'Trigger the official panel self-updater on each given node (downloads the latest release and restarts). Only enabled, online nodes are updated; offline/disabled ones are reported as skipped. Set "dev": true to move the nodes to the rolling per-commit dev channel instead of the latest stable release. Returns a per-node result list.',
|
||||
body: '{\n "ids": [1, 2, 3],\n "dev": false\n}',
|
||||
response: '{\n "success": true,\n "obj": [\n { "id": 1, "name": "de-1", "ok": true },\n { "id": 2, "name": "fr-1", "ok": false, "error": "node is offline" }\n ]\n}',
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Button, Card, Col, ConfigProvider, Input, Layout, Modal, Result, Row, Spin, Statistic, Typography, message } from 'antd';
|
||||
import { Alert, Button, Card, Checkbox, Col, ConfigProvider, Input, Layout, Modal, Result, Row, Spin, Statistic, Typography, message } from 'antd';
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
|
|
@ -21,6 +21,33 @@ import { setMessageInstance } from '@/utils/messageBus';
|
|||
import { HttpUtil } from '@/utils';
|
||||
import type { PanelUpdateInfo } from '../index/PanelUpdateModal';
|
||||
|
||||
// Confirm-dialog body that lets the operator pick the stable or dev channel for
|
||||
// a node panel update. Reports changes via onChange so the imperative
|
||||
// modal.confirm onOk can read the latest choice through a ref.
|
||||
function UpdateChannelChoice({ onChange }: { onChange: (dev: boolean) => void }) {
|
||||
const { t } = useTranslation();
|
||||
const [dev, setDev] = useState(false);
|
||||
return (
|
||||
<div>
|
||||
<p>{t('pages.nodes.updateConfirmContent')}</p>
|
||||
<Checkbox
|
||||
checked={dev}
|
||||
onChange={(e) => { setDev(e.target.checked); onChange(e.target.checked); }}
|
||||
>
|
||||
{t('pages.nodes.updateDevChannel')}
|
||||
</Checkbox>
|
||||
{dev && (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginTop: 8 }}
|
||||
message={t('pages.index.devChannelWarning')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function NodesPage() {
|
||||
const { t } = useTranslation();
|
||||
const { isDark, isUltra, antdThemeConfig } = useTheme();
|
||||
|
|
@ -136,8 +163,10 @@ export default function NodesPage() {
|
|||
await setEnable(node.id, next);
|
||||
}, [setEnable]);
|
||||
|
||||
const runUpdate = useCallback(async (ids: number[]) => {
|
||||
const msg = await updatePanels(ids);
|
||||
const devRef = useRef(false);
|
||||
|
||||
const runUpdate = useCallback(async (ids: number[], dev: boolean) => {
|
||||
const msg = await updatePanels(ids, dev);
|
||||
if (!msg?.success) {
|
||||
messageApi.error(msg?.msg || t('somethingWentWrong'));
|
||||
return;
|
||||
|
|
@ -156,12 +185,13 @@ export default function NodesPage() {
|
|||
}, [updatePanels, messageApi, t]);
|
||||
|
||||
const onUpdateNode = useCallback((node: NodeRecord) => {
|
||||
devRef.current = false;
|
||||
modal.confirm({
|
||||
title: t('pages.nodes.updateConfirmTitle', { count: 1 }),
|
||||
content: t('pages.nodes.updateConfirmContent'),
|
||||
content: <UpdateChannelChoice onChange={(v) => { devRef.current = v; }} />,
|
||||
okText: t('update'),
|
||||
cancelText: t('cancel'),
|
||||
onOk: () => runUpdate([node.id]),
|
||||
onOk: () => runUpdate([node.id], devRef.current),
|
||||
});
|
||||
}, [modal, t, runUpdate]);
|
||||
|
||||
|
|
@ -173,12 +203,13 @@ export default function NodesPage() {
|
|||
messageApi.warning(t('pages.nodes.toasts.updateNoneEligible'));
|
||||
return;
|
||||
}
|
||||
devRef.current = false;
|
||||
modal.confirm({
|
||||
title: t('pages.nodes.updateConfirmTitle', { count: eligible.length }),
|
||||
content: t('pages.nodes.updateConfirmContent'),
|
||||
content: <UpdateChannelChoice onChange={(v) => { devRef.current = v; }} />,
|
||||
okText: t('update'),
|
||||
cancelText: t('cancel'),
|
||||
onOk: () => runUpdate(eligible),
|
||||
onOk: () => runUpdate(eligible, devRef.current),
|
||||
});
|
||||
}, [modal, t, nodes, selectedIds, runUpdate, messageApi]);
|
||||
|
||||
|
|
|
|||
|
|
@ -318,6 +318,7 @@ func (a *NodeController) probe(c *gin.Context) {
|
|||
func (a *NodeController) updatePanel(c *gin.Context) {
|
||||
var req struct {
|
||||
Ids []int `json:"ids"`
|
||||
Dev bool `json:"dev"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
|
|
@ -327,7 +328,7 @@ func (a *NodeController) updatePanel(c *gin.Context) {
|
|||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), fmt.Errorf("no nodes selected"))
|
||||
return
|
||||
}
|
||||
results, err := a.nodeService.UpdatePanels(req.Ids)
|
||||
results, err := a.nodeService.UpdatePanels(req.Ids, req.Dev)
|
||||
jsonMsgObj(c, I18nWeb(c, "pages.nodes.toasts.updateStarted"), results, err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -206,9 +206,22 @@ func (a *ServerController) installXray(c *gin.Context) {
|
|||
jsonMsg(c, I18nWeb(c, "pages.index.xraySwitchVersionPopover"), err)
|
||||
}
|
||||
|
||||
// updatePanel starts a panel self-update to the latest release.
|
||||
// updatePanel starts a panel self-update. With no "dev" form value it follows
|
||||
// this panel's own channel setting; an explicit "dev" (sent by the master node
|
||||
// updater) overrides it for this run.
|
||||
func (a *ServerController) updatePanel(c *gin.Context) {
|
||||
err := a.panelService.StartUpdate()
|
||||
devParam := c.PostForm("dev")
|
||||
var err error
|
||||
if devParam == "" {
|
||||
err = a.panelService.StartUpdate()
|
||||
} else {
|
||||
dev, perr := strconv.ParseBool(devParam)
|
||||
if perr != nil {
|
||||
jsonMsg(c, "invalid data", perr)
|
||||
return
|
||||
}
|
||||
err = a.panelService.StartUpdateChannel(dev)
|
||||
}
|
||||
jsonMsg(c, I18nWeb(c, "pages.index.panelUpdateStartedPopover"), err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -538,9 +538,14 @@ func (r *Remote) RestartXray(ctx context.Context) error {
|
|||
|
||||
// UpdatePanel asks the node to run its own official self-updater (update.sh)
|
||||
// and restart onto the latest release. The node returns as soon as the job is
|
||||
// launched; the new version surfaces on the next heartbeat.
|
||||
func (r *Remote) UpdatePanel(ctx context.Context) error {
|
||||
_, err := r.do(ctx, http.MethodPost, "panel/api/server/updatePanel", nil)
|
||||
// launched; the new version surfaces on the next heartbeat. When dev is true the
|
||||
// node is moved to the rolling dev channel instead of the latest stable release.
|
||||
func (r *Remote) UpdatePanel(ctx context.Context, dev bool) error {
|
||||
var body any
|
||||
if dev {
|
||||
body = url.Values{"dev": {"true"}}
|
||||
}
|
||||
_, err := r.do(ctx, http.MethodPost, "panel/api/server/updatePanel", body)
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -637,7 +637,7 @@ type NodeUpdateResult struct {
|
|||
// UpdatePanels triggers the official self-updater on each given node. Only
|
||||
// enabled, online nodes are eligible — an offline node can't be reached, so it
|
||||
// is reported as skipped rather than silently dropped.
|
||||
func (s *NodeService) UpdatePanels(ids []int) ([]NodeUpdateResult, error) {
|
||||
func (s *NodeService) UpdatePanels(ids []int, dev bool) ([]NodeUpdateResult, error) {
|
||||
mgr := runtime.GetManager()
|
||||
if mgr == nil {
|
||||
return nil, fmt.Errorf("runtime manager unavailable")
|
||||
|
|
@ -662,7 +662,7 @@ func (s *NodeService) UpdatePanels(ids []int) ([]NodeUpdateResult, error) {
|
|||
break
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
updErr := remote.UpdatePanel(ctx)
|
||||
updErr := remote.UpdatePanel(ctx, dev)
|
||||
cancel()
|
||||
if updErr != nil {
|
||||
res.Error = updErr.Error()
|
||||
|
|
|
|||
|
|
@ -122,8 +122,19 @@ func getDevUpdateInfo() (*PanelUpdateInfo, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
// StartUpdate starts the official updater outside of the current web request.
|
||||
// StartUpdate starts the official updater using this panel's own channel setting.
|
||||
func (s *PanelService) StartUpdate() error {
|
||||
return s.startUpdate(devChannelActive())
|
||||
}
|
||||
|
||||
// StartUpdateChannel runs the updater against an explicitly chosen channel,
|
||||
// overriding the local dev-channel setting. Used by the master node updater so
|
||||
// a node can be moved to the dev channel from the central panel.
|
||||
func (s *PanelService) StartUpdateChannel(dev bool) error {
|
||||
return s.startUpdate(dev)
|
||||
}
|
||||
|
||||
func (s *PanelService) startUpdate(useDev bool) error {
|
||||
if runtime.GOOS != "linux" {
|
||||
return fmt.Errorf("panel web update is supported only on Linux installations")
|
||||
}
|
||||
|
|
@ -140,7 +151,7 @@ func (s *PanelService) StartUpdate() error {
|
|||
|
||||
mainFolder, serviceFolder := resolveUpdateFolders()
|
||||
updateTag := ""
|
||||
if devChannelActive() {
|
||||
if useDev {
|
||||
updateTag = devReleaseTag
|
||||
}
|
||||
updateScript := fmt.Sprintf("set -e; trap 'rm -f %s' EXIT; %s %s", shellQuote(scriptPath), shellQuote(bash), shellQuote(scriptPath))
|
||||
|
|
|
|||
|
|
@ -979,6 +979,7 @@
|
|||
"upToDate": "محدّث",
|
||||
"updateConfirmTitle": "تحديث {count} عقدة إلى أحدث إصدار؟",
|
||||
"updateConfirmContent": "كل عقدة محددة ستنزّل أحدث إصدار وتعيد التشغيل عليه. يتم تحديث العقد المفعّلة والمتصلة فقط.",
|
||||
"updateDevChannel": "التحديث إلى قناة التطوير (أحدث كومِت)",
|
||||
"testConnection": "اختبار الاتصال",
|
||||
"connectionOk": "الاتصال شغال ({ms} ms)",
|
||||
"connectionFailed": "فشل الاتصال",
|
||||
|
|
|
|||
|
|
@ -1098,6 +1098,7 @@
|
|||
"upToDate": "Up to date",
|
||||
"updateConfirmTitle": "Update {count} node(s) to the latest version?",
|
||||
"updateConfirmContent": "Each selected node downloads the latest release and restarts onto it. Only enabled, online nodes are updated.",
|
||||
"updateDevChannel": "Update to Dev channel (latest commit)",
|
||||
"testConnection": "Test Connection",
|
||||
"connectionOk": "Connection OK ({ms} ms)",
|
||||
"connectionFailed": "Connection failed",
|
||||
|
|
|
|||
|
|
@ -979,6 +979,7 @@
|
|||
"upToDate": "Actualizado",
|
||||
"updateConfirmTitle": "¿Actualizar {count} nodo(s) a la última versión?",
|
||||
"updateConfirmContent": "Cada nodo seleccionado descarga la última versión y se reinicia con ella. Solo se actualizan los nodos habilitados y en línea.",
|
||||
"updateDevChannel": "Actualizar al canal de desarrollo (último commit)",
|
||||
"testConnection": "Probar conexión",
|
||||
"connectionOk": "Conexión correcta ({ms} ms)",
|
||||
"connectionFailed": "Conexión fallida",
|
||||
|
|
|
|||
|
|
@ -979,6 +979,7 @@
|
|||
"upToDate": "بهروز",
|
||||
"updateConfirmTitle": "{count} نود به آخرین نسخه بهروزرسانی شوند؟",
|
||||
"updateConfirmContent": "هر نود انتخابشده آخرین نسخه را دانلود و روی آن ریاستارت میشود. فقط نودهای فعال و آنلاین بهروزرسانی میشوند.",
|
||||
"updateDevChannel": "بهروزرسانی به کانال دِو (آخرین کامیت)",
|
||||
"testConnection": "تست اتصال",
|
||||
"connectionOk": "اتصال موفق ({ms} میلیثانیه)",
|
||||
"connectionFailed": "اتصال ناموفق",
|
||||
|
|
|
|||
|
|
@ -979,6 +979,7 @@
|
|||
"upToDate": "Terbaru",
|
||||
"updateConfirmTitle": "Perbarui {count} node ke versi terbaru?",
|
||||
"updateConfirmContent": "Setiap node terpilih mengunduh rilis terbaru dan memulai ulang. Hanya node aktif dan online yang diperbarui.",
|
||||
"updateDevChannel": "Perbarui ke kanal dev (commit terbaru)",
|
||||
"testConnection": "Tes Koneksi",
|
||||
"connectionOk": "Koneksi OK ({ms} ms)",
|
||||
"connectionFailed": "Koneksi gagal",
|
||||
|
|
|
|||
|
|
@ -979,6 +979,7 @@
|
|||
"upToDate": "最新",
|
||||
"updateConfirmTitle": "{count} 個のノードを最新バージョンに更新しますか?",
|
||||
"updateConfirmContent": "選択した各ノードは最新リリースをダウンロードして再起動します。有効かつオンラインのノードのみが更新されます。",
|
||||
"updateDevChannel": "開発チャンネルに更新(最新コミット)",
|
||||
"testConnection": "接続テスト",
|
||||
"connectionOk": "接続OK ({ms} ms)",
|
||||
"connectionFailed": "接続に失敗しました",
|
||||
|
|
|
|||
|
|
@ -979,6 +979,7 @@
|
|||
"upToDate": "Atualizado",
|
||||
"updateConfirmTitle": "Atualizar {count} nó(s) para a versão mais recente?",
|
||||
"updateConfirmContent": "Cada nó selecionado baixa a versão mais recente e reinicia nela. Apenas nós ativos e online são atualizados.",
|
||||
"updateDevChannel": "Atualizar para o canal de desenvolvimento (último commit)",
|
||||
"testConnection": "Testar conexão",
|
||||
"connectionOk": "Conexão OK ({ms} ms)",
|
||||
"connectionFailed": "Falha na conexão",
|
||||
|
|
|
|||
|
|
@ -979,6 +979,7 @@
|
|||
"upToDate": "Актуально",
|
||||
"updateConfirmTitle": "Обновить {count} узлов до последней версии?",
|
||||
"updateConfirmContent": "Каждый выбранный узел загрузит последний релиз и перезапустится. Обновляются только включённые узлы в сети.",
|
||||
"updateDevChannel": "Обновить до канала разработки (последний коммит)",
|
||||
"testConnection": "Проверить соединение",
|
||||
"connectionOk": "Соединение в порядке ({ms} мс)",
|
||||
"connectionFailed": "Не удалось подключиться",
|
||||
|
|
|
|||
|
|
@ -979,6 +979,7 @@
|
|||
"upToDate": "Güncel",
|
||||
"updateConfirmTitle": "{count} düğüm en son sürüme güncellensin mi?",
|
||||
"updateConfirmContent": "Seçilen her düğüm en son sürümü indirir ve yeniden başlatılır. Yalnızca etkin ve çevrimiçi düğümler güncellenir.",
|
||||
"updateDevChannel": "Dev kanalına güncelle (son commit)",
|
||||
"testConnection": "Bağlantıyı Test Et",
|
||||
"connectionOk": "Bağlantı tamam ({ms} ms)",
|
||||
"connectionFailed": "Bağlantı başarısız",
|
||||
|
|
|
|||
|
|
@ -979,6 +979,7 @@
|
|||
"upToDate": "Актуально",
|
||||
"updateConfirmTitle": "Оновити {count} вузлів до останньої версії?",
|
||||
"updateConfirmContent": "Кожен вибраний вузол завантажить останній реліз і перезапуститься. Оновлюються лише увімкнені вузли в мережі.",
|
||||
"updateDevChannel": "Оновити до каналу розробки (останній коміт)",
|
||||
"testConnection": "Перевірити з'єднання",
|
||||
"connectionOk": "З'єднання в порядку ({ms} мс)",
|
||||
"connectionFailed": "Помилка з'єднання",
|
||||
|
|
|
|||
|
|
@ -979,6 +979,7 @@
|
|||
"upToDate": "Mới nhất",
|
||||
"updateConfirmTitle": "Cập nhật {count} node lên phiên bản mới nhất?",
|
||||
"updateConfirmContent": "Mỗi node đã chọn sẽ tải bản phát hành mới nhất và khởi động lại. Chỉ các node đang bật và trực tuyến được cập nhật.",
|
||||
"updateDevChannel": "Cập nhật lên kênh phát triển (commit mới nhất)",
|
||||
"testConnection": "Kiểm tra kết nối",
|
||||
"connectionOk": "Kết nối OK ({ms} ms)",
|
||||
"connectionFailed": "Kết nối thất bại",
|
||||
|
|
|
|||
|
|
@ -979,6 +979,7 @@
|
|||
"upToDate": "已是最新",
|
||||
"updateConfirmTitle": "将 {count} 个节点更新到最新版本?",
|
||||
"updateConfirmContent": "每个所选节点会下载最新版本并重启。仅更新已启用且在线的节点。",
|
||||
"updateDevChannel": "更新到开发通道(最新提交)",
|
||||
"testConnection": "测试连接",
|
||||
"connectionOk": "连接正常 ({ms} ms)",
|
||||
"connectionFailed": "连接失败",
|
||||
|
|
|
|||
|
|
@ -979,6 +979,7 @@
|
|||
"upToDate": "已是最新",
|
||||
"updateConfirmTitle": "將 {count} 個節點更新到最新版本?",
|
||||
"updateConfirmContent": "每個所選節點會下載最新版本並重新啟動。僅更新已啟用且在線的節點。",
|
||||
"updateDevChannel": "更新到開發通道(最新提交)",
|
||||
"testConnection": "測試連線",
|
||||
"connectionOk": "連線正常 ({ms} ms)",
|
||||
"connectionFailed": "連線失敗",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue