+
{JSON.stringify(value ?? {}, null, 2)}
upload=N; download=N; total=N; expire=TS' },
+ {
+ name: 'Subscription-Userinfo',
+ desc: 'Traffic and expiry: upload=N; download=N; total=N; expire=TS',
+ },
{ name: 'Profile-Title', desc: 'Base64-encoded subscription display name' },
{ name: 'Profile-Web-Page-Url', desc: 'Link to the subscription info page' },
{ name: 'Support-Url', desc: 'Support contact URL configured in settings' },
- { name: 'Profile-Update-Interval', desc: 'Suggested polling interval in minutes (e.g. 10)' },
+ {
+ name: 'Profile-Update-Interval',
+ desc: 'Suggested polling interval in minutes (e.g. 10)',
+ },
{ name: 'Announce', desc: 'Base64-encoded announcement string' },
- { name: 'Routing-Enable', desc: 'true or false — whether routing rules are included' },
- { name: 'Routing', desc: 'Global routing rules for client apps that support them (e.g. Happ)' },
+ {
+ name: 'Routing-Enable',
+ desc: 'true or false — whether routing rules are included',
+ },
+ {
+ name: 'Routing',
+ desc: 'Global routing rules for client apps that support them (e.g. Happ)',
+ },
],
endpoints: [
{
method: 'GET',
path: '/{subPath}:subid',
- summary: 'Return base64-encoded subscription links for all enabled clients matching the subscription ID. When the request has an Accept: text/html header or ?html=1, renders a styled info page instead. With ?format=info, returns the page view-model as JSON (traffic, expiry, online status; no links) for live polling. Default path: /sub/:subid.',
+ summary:
+ 'Return base64-encoded subscription links for all enabled clients matching the subscription ID. When the request has an Accept: text/html header or ?html=1, renders a styled info page instead. With ?format=info, returns the page view-model as JSON (traffic, expiry, online status; no links) for live polling. Default path: /sub/:subid.',
params: [
{ name: 'subid', in: 'path', type: 'string', desc: 'Client subscription ID.' },
- { name: 'format', in: 'query', type: 'string', optional: true, desc: 'Set to "info" to get the subscription status view-model as JSON instead of the links.' },
+ {
+ name: 'format',
+ in: 'query',
+ type: 'string',
+ optional: true,
+ desc: 'Set to "info" to get the subscription status view-model as JSON instead of the links.',
+ },
],
},
{
method: 'GET',
path: '/{jsonPath}:subid',
- summary: 'Return subscription as a JSON array of proxy configs (one per enabled client). Only when JSON subscription is enabled in settings. Default path: /json/:subid.',
- params: [
- { name: 'subid', in: 'path', type: 'string', desc: 'Client subscription ID.' },
- ],
+ summary:
+ 'Return subscription as a JSON array of proxy configs (one per enabled client). Only when JSON subscription is enabled in settings. Default path: /json/:subid.',
+ params: [{ name: 'subid', in: 'path', type: 'string', desc: 'Client subscription ID.' }],
},
{
method: 'GET',
path: '/{clashPath}:subid',
- summary: 'Return subscription as a Clash/Mihomo-compatible YAML config, including configured global Clash routing rules. Only when Clash subscription is enabled in settings. Default path: /clash/:subid.',
- params: [
- { name: 'subid', in: 'path', type: 'string', desc: 'Client subscription ID.' },
- ],
+ summary:
+ 'Return subscription as a Clash/Mihomo-compatible YAML config, including configured global Clash routing rules. Only when Clash subscription is enabled in settings. Default path: /clash/:subid.',
+ params: [{ name: 'subid', in: 'path', type: 'string', desc: 'Client subscription ID.' }],
},
],
},
@@ -1594,30 +2208,37 @@ export const sections: readonly Section[] = [
{
method: 'GET',
path: '/ws',
- summary: 'Upgrade an HTTP connection to a WebSocket. Requires an authenticated session cookie (Bearer token auth is not supported here). Returns 101 Switching Protocols on success. The server then pushes JSON messages described below.',
+ summary:
+ 'Upgrade an HTTP connection to a WebSocket. Requires an authenticated session cookie (Bearer token auth is not supported here). Returns 101 Switching Protocols on success. The server then pushes JSON messages described below.',
},
{
method: 'WS',
path: '→ type: status',
- summary: 'Server health snapshot pushed every 2 seconds. Contains CPU, memory, swap, disk, network IO, load, and Xray state — same shape as GET /panel/api/server/status.',
- response: '{\n "type": "status",\n "data": { "cpu": 12.5, "mem": { "current": 2147483648, "total": 8589934592 }, "xray": { "state": "running" } }\n}',
+ summary:
+ 'Server health snapshot pushed every 2 seconds. Contains CPU, memory, swap, disk, network IO, load, and Xray state — same shape as GET /panel/api/server/status.',
+ response:
+ '{\n "type": "status",\n "data": { "cpu": 12.5, "mem": { "current": 2147483648, "total": 8589934592 }, "xray": { "state": "running" } }\n}',
},
{
method: 'WS',
path: '→ type: xrayState',
- summary: 'Xray process state change. Fired when Xray starts, stops, or encounters an error.',
+ summary:
+ 'Xray process state change. Fired when Xray starts, stops, or encounters an error.',
response: '{\n "type": "xrayState",\n "data": "running"\n}',
},
{
method: 'WS',
path: '→ type: notification',
- summary: 'In-panel toast notification. Fired on Xray stop/restart, DB import, panel restart, etc.',
- response: '{\n "type": "notification",\n "title": "Xray service restarted",\n "body": "Xray has been restarted successfully",\n "severity": "success"\n}',
+ summary:
+ 'In-panel toast notification. Fired on Xray stop/restart, DB import, panel restart, etc.',
+ response:
+ '{\n "type": "notification",\n "title": "Xray service restarted",\n "body": "Xray has been restarted successfully",\n "severity": "success"\n}',
},
{
method: 'WS',
path: '→ type: invalidate',
- summary: 'Instructs the UI to re-fetch a resource. Fired when another admin session modifies data (e.g. toggling inbound enable).',
+ summary:
+ 'Instructs the UI to re-fetch a resource. Fired when another admin session modifies data (e.g. toggling inbound enable).',
response: '{\n "type": "invalidate",\n "resource": "inbounds"\n}',
},
],
diff --git a/frontend/src/pages/clients/BulkAddToGroupModal.tsx b/frontend/src/pages/clients/BulkAddToGroupModal.tsx
index 6e4702b77..e36cd2e4a 100644
--- a/frontend/src/pages/clients/BulkAddToGroupModal.tsx
+++ b/frontend/src/pages/clients/BulkAddToGroupModal.tsx
@@ -42,7 +42,9 @@ export default function BulkAddToGroupModal({
const result = await onSubmit(next);
if (result) {
const affected = result.affected ?? 0;
- messageApi.success(t('pages.clients.addToGroupSuccessToast', { count: affected, group: next }));
+ messageApi.success(
+ t('pages.clients.addToGroupSuccessToast', { count: affected, group: next }),
+ );
onOpenChange(false);
}
} finally {
diff --git a/frontend/src/pages/clients/BulkAttachInboundsModal.tsx b/frontend/src/pages/clients/BulkAttachInboundsModal.tsx
index 81730f9d4..b154d4346 100644
--- a/frontend/src/pages/clients/BulkAttachInboundsModal.tsx
+++ b/frontend/src/pages/clients/BulkAttachInboundsModal.tsx
@@ -7,7 +7,15 @@ import type { InboundOption } from '@/hooks/useClients';
import { formatInboundLabel } from '@/lib/inbounds/label';
import type { BulkAttachResult } from '@/schemas/client';
-const MULTI_USER_PROTOCOLS = new Set(['vmess', 'vless', 'trojan', 'hysteria', 'shadowsocks', 'wireguard', 'mtproto']);
+const MULTI_USER_PROTOCOLS = new Set([
+ 'vmess',
+ 'vless',
+ 'trojan',
+ 'hysteria',
+ 'shadowsocks',
+ 'wireguard',
+ 'mtproto',
+]);
interface BulkAttachInboundsModalProps {
open: boolean;
diff --git a/frontend/src/pages/clients/BulkDetachInboundsModal.tsx b/frontend/src/pages/clients/BulkDetachInboundsModal.tsx
index 0cdcabe6f..3be36699b 100644
--- a/frontend/src/pages/clients/BulkDetachInboundsModal.tsx
+++ b/frontend/src/pages/clients/BulkDetachInboundsModal.tsx
@@ -7,7 +7,15 @@ import type { InboundOption } from '@/hooks/useClients';
import { formatInboundLabel } from '@/lib/inbounds/label';
import type { BulkDetachResult } from '@/schemas/client';
-const MULTI_USER_PROTOCOLS = new Set(['vmess', 'vless', 'trojan', 'hysteria', 'shadowsocks', 'wireguard', 'mtproto']);
+const MULTI_USER_PROTOCOLS = new Set([
+ 'vmess',
+ 'vless',
+ 'trojan',
+ 'hysteria',
+ 'shadowsocks',
+ 'wireguard',
+ 'mtproto',
+]);
interface BulkDetachInboundsModalProps {
open: boolean;
diff --git a/frontend/src/pages/clients/ClientBulkAddModal.tsx b/frontend/src/pages/clients/ClientBulkAddModal.tsx
index 218999206..1c4b5e4d0 100644
--- a/frontend/src/pages/clients/ClientBulkAddModal.tsx
+++ b/frontend/src/pages/clients/ClientBulkAddModal.tsx
@@ -1,6 +1,18 @@
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { AutoComplete, Button, Form, Input, InputNumber, Modal, Select, Space, Switch, Tooltip, message } from 'antd';
+import {
+ AutoComplete,
+ Button,
+ Form,
+ Input,
+ InputNumber,
+ Modal,
+ Select,
+ Space,
+ Switch,
+ Tooltip,
+ message,
+} from 'antd';
import { ReloadOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import type { Dayjs } from 'dayjs';
@@ -18,7 +30,12 @@ import { ClientBulkAddFormSchema, type ClientBulkAddFormValues } from '@/schemas
const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL);
const MULTI_CLIENT_PROTOCOLS = new Set([
- 'shadowsocks', 'vless', 'vmess', 'trojan', 'hysteria', 'wireguard',
+ 'shadowsocks',
+ 'vless',
+ 'vmess',
+ 'trojan',
+ 'hysteria',
+ 'wireguard',
]);
const EMPTY: ClientBulkAddFormValues = {
@@ -83,7 +100,6 @@ export default function ClientBulkAddModal({
methods.reset(EMPTY);
setDelayedStart(false);
-
}, [open, methods]);
const flowCapableIds = useMemo(() => {
@@ -110,18 +126,18 @@ export default function ClientBulkAddModal({
useEffect(() => {
if (!showFlow && flow) {
-
methods.setValue('flow', '');
}
}, [showFlow, flow, methods]);
const inboundOptions = useMemo(
- () => (inbounds || [])
- .filter((ib) => MULTI_CLIENT_PROTOCOLS.has(ib.protocol || ''))
- .map((ib) => ({
- label: formatInboundLabel(ib.tag, ib.remark),
- value: ib.id,
- })),
+ () =>
+ (inbounds || [])
+ .filter((ib) => MULTI_CLIENT_PROTOCOLS.has(ib.protocol || ''))
+ .map((ib) => ({
+ label: formatInboundLabel(ib.tag, ib.remark),
+ value: ib.id,
+ })),
[inbounds],
);
@@ -177,7 +193,7 @@ export default function ClientBulkAddModal({
? RandomUtil.randomShadowsocksPassword(ss2022Method)
: RandomUtil.randomLowerAndNum(16),
auth: RandomUtil.randomLowerAndNum(16),
- flow: showFlow ? (current.flow || '') : '',
+ flow: showFlow ? current.flow || '' : '',
totalGB: Math.round((current.totalGB || 0) * SizeFormatter.ONE_GB),
expiryTime: current.expiryTime,
reset: Number(current.reset) || 0,
@@ -201,9 +217,11 @@ export default function ClientBulkAddModal({
if (failed === 0 && msg?.success) {
messageApi.success(t('pages.clients.toasts.bulkCreated', { count: ok }));
} else {
- messageApi.warning(firstError
- ? `${t('pages.clients.toasts.bulkCreatedMixed', { ok, failed })} — ${firstError}`
- : t('pages.clients.toasts.bulkCreatedMixed', { ok, failed }));
+ messageApi.warning(
+ firstError
+ ? `${t('pages.clients.toasts.bulkCreatedMixed', { ok, failed })} — ${firstError}`
+ : t('pages.clients.toasts.bulkCreatedMixed', { ok, failed }),
+ );
}
onSaved?.();
onOpenChange(false);
@@ -241,7 +259,8 @@ export default function ClientBulkAddModal({
options={inboundOptions}
placeholder={t('pages.clients.selectInbound')}
showSearch={{
- filterOption: (input, option) => ((option?.label as string) || '').toLowerCase().includes(input.toLowerCase()),
+ filterOption: (input, option) =>
+ ((option?.label as string) || '').toLowerCase().includes(input.toLowerCase()),
}}
/>
{link.link}
@@ -425,20 +503,44 @@ export default function InboundInfoModal({
{encryptionLabel}
{wireguardConfigs[idx]}
- {wireguardLinks[idx]}
- {wireguardConfigs[idx]}
+ {wireguardLinks[idx]}
+ {link.link}
@@ -877,7 +1202,14 @@ export default function InboundInfoModal({
tabItems.push({ key: 'inbound', label: t('pages.xray.rules.inbound'), children: inboundTab });
return (
- {t('pages.nodes.updateConfirmContent')}
{t('pages.nodes.apiTokenHint')}
+{t('pages.nodes.apiTokenHint')}
-- {t('pages.settings.security.apiTokenCreatedNotice') - || 'Copy this token now. For security it is not stored in readable form and will not be shown again.'} + {t('pages.settings.security.apiTokenCreatedNotice') || + 'Copy this token now. For security it is not stored in readable form and will not be shown again.'}
{createdToken?.token}
- {t('pages.settings.security.twoFactorModalSteps')}
-{t('pages.settings.security.twoFactorModalFirstStep')}
-{t('pages.settings.security.twoFactorModalSteps')}
+{t('pages.settings.security.twoFactorModalFirstStep')}
+{t('pages.settings.security.twoFactorModalSecondStep')}
+ setEnteredCode(e.target.value)} + style={{ width: '100%' }} + aria-label={t('twoFactorCode')} /> - {token} -{t('pages.settings.security.twoFactorModalSecondStep')}
- setEnteredCode(e.target.value)} style={{ width: '100%' }} aria-label={t('twoFactorCode')} /> - > - ) : ( - <> -{description}
- setEnteredCode(e.target.value)} style={{ width: '100%' }} aria-label={t('twoFactorCode')} /> - > - )} + > + ) : ( + <> +{description}
+ setEnteredCode(e.target.value)} + style={{ width: '100%' }} + aria-label={t('twoFactorCode')} + /> + > + )}| {t('pages.xray.nord.accessToken')} | -{nordData.token} | -
| {t('pages.xray.nord.privateKey')} | -{nordData.private_key} | -
| {t('pages.xray.nord.accessToken')} | +{nordData.token} | +
| {t('pages.xray.nord.privateKey')} | +{nordData.private_key} | +
| {t('pages.xray.warp.accessToken')} | -{warpData?.access_token} | -
| {t('pages.xray.warp.deviceId')} | -{warpData?.device_id} | -
| {t('pages.xray.warp.licenseKey')} | -{warpData?.license_key} | -
| {t('pages.xray.warp.privateKey')} | -{warpData?.private_key} | -
| {t('pages.xray.warp.accessToken')} | +{warpData?.access_token} | +
| {t('pages.xray.warp.deviceId')} | +{warpData?.device_id} | +
| {t('pages.xray.warp.licenseKey')} | +{warpData?.license_key} | +
| {t('pages.xray.warp.privateKey')} | +{warpData?.private_key} | +
| {t('pages.xray.warp.deviceName')} | -{warpConfig?.name} | -||
| {t('pages.xray.warp.deviceModel')} | -{warpConfig?.model} | -||
| {t('pages.xray.warp.deviceEnabled')} | -{String(warpConfig?.enabled)} | -||
| {t('pages.xray.warp.accountType')} | -{warpConfig.account.account_type} | -||
| {t('pages.xray.warp.role')} | -{warpConfig.account.role} | -||
| {t('pages.xray.warp.warpPlusData')} | -{SizeFormatter.sizeFormat(warpConfig.account.premium_data)} | -||
| {t('pages.xray.warp.quota')} | -{SizeFormatter.sizeFormat(warpConfig.account.quota)} | -
| {t('pages.xray.warp.deviceName')} | +{warpConfig?.name} | +||
| {t('pages.xray.warp.deviceModel')} | +{warpConfig?.model} | +||
| {t('pages.xray.warp.deviceEnabled')} | +{String(warpConfig?.enabled)} | +||
| {t('pages.xray.warp.usage')} | -{SizeFormatter.sizeFormat(warpConfig.account.usage)} | +{t('pages.xray.warp.accountType')} | +{warpConfig.account.account_type} |