diff --git a/frontend/src/hooks/usePageTitle.ts b/frontend/src/hooks/usePageTitle.ts index 48011a0..b14e7c0 100644 --- a/frontend/src/hooks/usePageTitle.ts +++ b/frontend/src/hooks/usePageTitle.ts @@ -20,7 +20,7 @@ export function usePageTitle() { useEffect(() => { const key = TITLE_KEYS[pathname]; - const title = key ? t(key) : '3X-UI'; + const title = key ? t(key) : 'L-UI'; const host = window.location.hostname; document.title = host ? `${host} - ${title}` : title; }, [pathname, t]); diff --git a/frontend/src/layouts/AppSidebar.tsx b/frontend/src/layouts/AppSidebar.tsx index 0861a4a..5d57094 100644 --- a/frontend/src/layouts/AppSidebar.tsx +++ b/frontend/src/layouts/AppSidebar.tsx @@ -356,7 +356,7 @@ export default function AppSidebar() { >
- {railCollapsed ? '3X' : '3X-UI'} + {railCollapsed ? 'L' : 'L-UI'}
{!railCollapsed && (
@@ -420,7 +420,7 @@ export default function AppSidebar() { >
- 3X-UI + L-UI
diff --git a/frontend/src/lib/panel-version.ts b/frontend/src/lib/panel-version.ts index 598a1e9..cd9faf5 100644 --- a/frontend/src/lib/panel-version.ts +++ b/frontend/src/lib/panel-version.ts @@ -22,6 +22,9 @@ function parseVersionParts(version: string): [number, number, number] | null { export function formatPanelVersion(version: string | undefined | null): string { const v = (version || '').trim(); if (!v) return ''; + // This fork's display versions (master "V1.0.0", agent "A1.0.0") are + // shown exactly as written — never re-prefixed or re-cased. + if (/^[VA]\d+\.\d+\.\d+$/.test(v)) return v; const normalized = v.replace(/^v/i, ''); return /^\d/.test(normalized) ? `v${normalized}` : v; } diff --git a/frontend/src/pages/index/IndexPage.tsx b/frontend/src/pages/index/IndexPage.tsx index 4f7c58d..f6d9e9c 100644 --- a/frontend/src/pages/index/IndexPage.tsx +++ b/frontend/src/pages/index/IndexPage.tsx @@ -172,8 +172,17 @@ export default function IndexPage() { setBackupOpen(true)} onOpenPanelUpdate={() => setPanelUpdateOpen(true)} diff --git a/frontend/src/pages/index/PanelUpdateModal.tsx b/frontend/src/pages/index/PanelUpdateModal.tsx index c785cc9..27f23dd 100644 --- a/frontend/src/pages/index/PanelUpdateModal.tsx +++ b/frontend/src/pages/index/PanelUpdateModal.tsx @@ -17,6 +17,11 @@ export interface PanelUpdateInfo { currentCommit?: string; latestCommit?: string; updateAvailable: boolean; + // V-series display versions (items 18/20); the stable channel's update + // decision and labels use these when present. + displayCurrentVersion?: string; + displayLatestVersion?: string; + displayUpdateAvailable?: boolean; } interface BusyEvent { @@ -47,6 +52,13 @@ export default function PanelUpdateModal({ const isDev = info.channel === 'dev'; + // On the stable channel the V-series display comparison decides (item + // 20); the dev channel keeps its commit-based decision. + const masterUpdateAvailable = isDev + ? info.updateAvailable + : (info.displayUpdateAvailable ?? info.updateAvailable); + const displayLatest = info.displayLatestVersion || info.latestVersion; + async function pollUpdateStatus(expectedRunId: string): Promise { await PromiseUtil.sleep(5000); const deadline = Date.now() + 90_000; @@ -85,13 +97,14 @@ export default function PanelUpdateModal({ title: t('pages.index.panelUpdateDialog'), content: t('pages.index.panelUpdateDialogDesc').replace( '#version#', - info.latestVersion || '', + isDev ? info.latestVersion || '' : displayLatest || '', ), okText: t('confirm'), cancelText: t('cancel'), onOk: async () => { const baseTip = t('pages.index.dontRefresh'); - const tip = info.latestVersion ? `${baseTip} (${info.latestVersion})` : baseTip; + const tipVersion = isDev ? info.latestVersion : displayLatest; + const tip = tipVersion ? `${baseTip} (${tipVersion})` : baseTip; onClose(); onBusy({ busy: true, tip }); const result = await HttpUtil.post<{ runId: string }>('/panel/api/server/updatePanel'); @@ -128,7 +141,7 @@ export default function PanelUpdateModal({ <> {contextHolder} - {info.updateAvailable && ( + {masterUpdateAvailable && ( )}
- {info.updateAvailable ? ( + {masterUpdateAvailable ? (
{isDev ? t('pages.index.latestCommit') : t('pages.index.latestPanelVersion')} - {(isDev ? info.latestCommit : info.latestVersion) || '-'} + + {(isDev ? info.latestCommit : formatPanelVersion(displayLatest)) || '-'} +
) : (
@@ -184,7 +199,7 @@ export default function PanelUpdateModal({
-
-
-
Xray
-
- {TimeFormatter.formatSecond(status.appStats.uptime)} -
-
- -
-
OS
-
{TimeFormatter.formatSecond(status.uptime)}
-
+ {/* The lite master runs no local xray core, so the uptime card + shows the OS uptime only (the Xray half always read 0s). */} +
+
OS
+
{TimeFormatter.formatSecond(status.uptime)}
diff --git a/frontend/src/pages/login/LoginPage.tsx b/frontend/src/pages/login/LoginPage.tsx index 59f8360..4183da3 100644 --- a/frontend/src/pages/login/LoginPage.tsx +++ b/frontend/src/pages/login/LoginPage.tsx @@ -179,7 +179,7 @@ export default function LoginPage() { ) : (
- 3X-UI + L-UI

diff --git a/frontend/src/pages/nodes/NodeFormModal.tsx b/frontend/src/pages/nodes/NodeFormModal.tsx index 5aefab0..ab583a8 100644 --- a/frontend/src/pages/nodes/NodeFormModal.tsx +++ b/frontend/src/pages/nodes/NodeFormModal.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Alert, @@ -16,7 +16,7 @@ import { import { FormProvider, useForm, useWatch } from 'react-hook-form'; import type { NodeRecord } from '@/api/queries/useNodesQuery'; import type { RemoteInboundOption } from '@/api/queries/useNodeMutations'; -import type { Msg } from '@/utils'; +import { HttpUtil, type Msg } from '@/utils'; import { NodeFormSchema, type NodeFormValues, type ProbeResult } from '@/schemas/node'; import { FormField, rhfZodValidate } from '@/components/form/rhf'; import { useOutboundTagGroups } from '@/api/queries/useOutboundTags'; @@ -81,6 +81,17 @@ export default function NodeFormModal({ const [agentToken, setAgentToken] = useState(null); const [agentNodeName, setAgentNodeName] = useState(''); const [agentMasterUrl, setAgentMasterUrl] = useState(''); + // Edit mode (item 19): the stored install material for an existing agent + // node, fetched from the master; null until loaded / for non-agent nodes. + // Keyed by node id so a stale result from a previously edited node is + // never shown (visibility is derived during render below). + const [editInstall, setEditInstall] = useState<{ + nodeId: number; + name: string; + token: string; + hasToken: boolean; + } | null>(null); + const [editMasterUrl, setEditMasterUrl] = useState(''); const scheme = useWatch({ control: methods.control, name: 'scheme' }) ?? 'https'; const tlsVerifyMode = useWatch({ control: methods.control, name: 'tlsVerifyMode' }) ?? 'verify'; const inboundSyncMode = useWatch({ control: methods.control, name: 'inboundSyncMode' }) ?? 'all'; @@ -136,6 +147,39 @@ export default function NodeFormModal({ setTestResult(null); } + // Load the stored install material when editing an agent node so the + // full install command can be re-displayed (item 19). Nodes created + // before tokens were stored answer hasToken=false until the agent's + // next authenticated request backfills the token. + const editNodeId = mode === 'edit' && isAgent ? (node?.id ?? 0) : 0; + useEffect(() => { + if (!open || !editNodeId) return; + let cancelled = false; + HttpUtil.get<{ name: string; token: string; hasToken: boolean }>( + `/panel/api/agent/install/${editNodeId}`, + ).then((msg) => { + if (!cancelled && msg?.success && msg.obj) { + setEditInstall({ nodeId: editNodeId, ...msg.obj }); + setEditMasterUrl(currentPanelMasterUrl()); + } + }); + return () => { + cancelled = true; + }; + }, [open, editNodeId]); + + const shownInstall = editInstall && editInstall.nodeId === editNodeId ? editInstall : null; + + const editInstallCmd = useMemo(() => { + if (!shownInstall || !editMasterUrl) return ''; + const base = + `bash <(curl -fL https://git.881218.xyz/rogue/3x-ui-agent/raw/branch/main/install-agent.sh)` + + ` --master ${editMasterUrl}`; + return shownInstall.hasToken + ? `${base} --token ${shownInstall.token} --name ${shownInstall.name}` + : `${base} --name ${shownInstall.name}`; + }, [shownInstall, editMasterUrl]); + const title = useMemo( () => (mode === 'edit' ? t('pages.nodes.editNode') : t('pages.nodes.addNode')), [mode, t], @@ -391,6 +435,43 @@ export default function NodeFormModal({ )} + {mode === 'edit' && isAgent && shownInstall && ( +
+
{t('pages.nodes.agentInstallCommand')}
+

{t('pages.nodes.agentMasterUrlHint')}

+ setEditMasterUrl(e.target.value.trim())} + style={{ marginBottom: 12, fontFamily: 'monospace' }} + /> + {!shownInstall.hasToken && ( + + )} + + +
+ )} + {!isAgent && ( record.transitive ? ( {isUpdateEligible(record) && ( - +