mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-16 23:27:14 +00:00
docs(api): align OpenAPI with runtime contracts (#6409)
Document the cookie-authenticated WebSocket upgrade and its emitted envelopes without exporting pseudo-paths. Align REST response schemas, paged-client filters, and subscription HEAD operations with their runtime implementations, then regenerate frontend and docs artifacts.
This commit is contained in:
@@ -44,6 +44,29 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.api-docs-page .websocket-events {
|
||||
margin-bottom: 16px;
|
||||
padding: 20px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--ant-color-border-secondary);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.api-docs-page .websocket-events h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.api-docs-page .websocket-events pre {
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
max-height: 360px;
|
||||
overflow: auto;
|
||||
color: var(--sw-text, inherit);
|
||||
background: var(--sw-bg-code, var(--ant-color-fill-quaternary));
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.api-docs-page .swagger-ui {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ConfigProvider, Layout } from 'antd';
|
||||
import { Card, Col, ConfigProvider, Layout, Row, Typography } from 'antd';
|
||||
import SwaggerUI from 'swagger-ui-react';
|
||||
import 'swagger-ui-react/swagger-ui.css';
|
||||
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import AppSidebar from '@/layouts/AppSidebar';
|
||||
import { EXAMPLES } from '@/generated/examples';
|
||||
import { buildWebSocketEvents } from './websocket-events';
|
||||
import './ApiDocsPage.css';
|
||||
|
||||
const basePath = window.X_UI_BASE_PATH || '';
|
||||
const openApiUrl = `${basePath}panel/api/openapi.json`;
|
||||
const websocketEvents = buildWebSocketEvents(EXAMPLES);
|
||||
|
||||
export default function ApiDocsPage() {
|
||||
const { isDark, isUltra, antdThemeConfig } = useTheme();
|
||||
@@ -29,6 +32,27 @@ export default function ApiDocsPage() {
|
||||
|
||||
<Layout className="content-shell">
|
||||
<Layout.Content className="content-area">
|
||||
<section className="websocket-events" aria-labelledby="websocket-events-title">
|
||||
<Typography.Title id="websocket-events-title" level={2}>
|
||||
WebSocket events
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph>
|
||||
After the cookie-authenticated <Typography.Text code>GET /ws</Typography.Text>{' '}
|
||||
upgrade, every server message uses{' '}
|
||||
<Typography.Text code>{'{ type, payload, time }'}</Typography.Text>. The time value
|
||||
is Unix milliseconds.
|
||||
</Typography.Paragraph>
|
||||
<Row gutter={[12, 12]}>
|
||||
{websocketEvents.map((event) => (
|
||||
<Col key={event.type} xs={24} sm={12} xl={8}>
|
||||
<Card size="small" title={<Typography.Text code>{event.type}</Typography.Text>}>
|
||||
<Typography.Paragraph>{event.summary}</Typography.Paragraph>
|
||||
<pre>{JSON.stringify(event.example, null, 2)}</pre>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</section>
|
||||
<div className="docs-wrapper" role="region" aria-label={t('menu.apiDocs')}>
|
||||
<SwaggerUI
|
||||
url={openApiUrl}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'WS';
|
||||
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD';
|
||||
export type ParamLocation =
|
||||
| 'path'
|
||||
| 'query'
|
||||
@@ -28,6 +28,7 @@ export interface EndpointParam {
|
||||
defaultValue?: string | number | boolean;
|
||||
minLength?: number;
|
||||
pattern?: string;
|
||||
enum?: readonly (string | number | boolean)[];
|
||||
}
|
||||
|
||||
export interface Endpoint {
|
||||
@@ -45,6 +46,9 @@ export interface Endpoint {
|
||||
bodyRequiredOneOf?: string[];
|
||||
responseSchema?: string;
|
||||
responseSchemaArray?: boolean;
|
||||
responseObjectSchema?: Record<string, unknown>;
|
||||
responses?: Record<string, Record<string, unknown>>;
|
||||
security?: readonly Record<string, readonly string[]>[];
|
||||
}
|
||||
|
||||
export interface SubscriptionHeader {
|
||||
@@ -172,6 +176,12 @@ const subBalancerBodyParams: EndpointParam[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const subscriptionHeadResponses = {
|
||||
'200': { description: 'Subscription is available. Headers match GET; no response body.' },
|
||||
'404': { description: 'No enabled client matches the subscription ID.' },
|
||||
'500': { description: 'Subscription generation failed.' },
|
||||
};
|
||||
|
||||
export const sections: readonly Section[] = [
|
||||
{
|
||||
id: 'authentication',
|
||||
@@ -556,7 +566,7 @@ export const sections: readonly Section[] = [
|
||||
method: 'GET',
|
||||
path: '/panel/api/server/getNewUUID',
|
||||
summary: 'Generate a fresh UUID v4. Convenience helper for client IDs.',
|
||||
response: '{\n "success": true,\n "obj": "550e8400-e29b-41d4-a716-446655440000"\n}',
|
||||
responseSchema: 'NewUUIDResponse',
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
@@ -584,18 +594,14 @@ export const sections: readonly Section[] = [
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/panel/api/server/getNewmldsa65',
|
||||
summary:
|
||||
'Generate a new ML-DSA-65 keypair (post-quantum signature). Returns {privateKey, publicKey, seed}.',
|
||||
response:
|
||||
'{\n "success": true,\n "obj": {\n "privateKey": "mdsa65priv...",\n "publicKey": "mdsa65pub...",\n "seed": "random-seed..."\n }\n}',
|
||||
summary: 'Generate a new ML-DSA-65 keypair. Returns {seed, verify}.',
|
||||
responseSchema: 'MLDSA65Response',
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/panel/api/server/getNewmlkem768',
|
||||
summary:
|
||||
'Generate a new ML-KEM-768 keypair (post-quantum KEM). Returns {clientKey, serverKey}.',
|
||||
response:
|
||||
'{\n "success": true,\n "obj": {\n "clientKey": "mlkem768-client...",\n "serverKey": "mlkem768-server..."\n }\n}',
|
||||
summary: 'Generate a new ML-KEM-768 keypair. Returns {seed, client}.',
|
||||
responseSchema: 'MLKEM768Response',
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
@@ -703,8 +709,9 @@ export const sections: readonly Section[] = [
|
||||
},
|
||||
],
|
||||
body: 'level=info&syslog=false',
|
||||
responseObjectSchema: { type: 'array', items: { type: 'string' } },
|
||||
response:
|
||||
'{\n "success": true,\n "obj": "2025/01/01 12:00:00 [INFO] Server started\\n2025/01/01 12:00:01 [INFO] Xray is running"\n}',
|
||||
'{\n "success": true,\n "obj": [\n "2025/01/01 12:00:00 [INFO] Server started",\n "2025/01/01 12:00:01 [INFO] Xray is running"\n ]\n}',
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
@@ -742,8 +749,8 @@ export const sections: readonly Section[] = [
|
||||
},
|
||||
],
|
||||
body: 'filter=error&showDirect=false&showBlocked=true&showProxy=true',
|
||||
response:
|
||||
'{\n "success": true,\n "obj": "2025/01/01 12:00:00 rejected vless proxy example.com reason: no valid user\\n2025/01/01 12:00:01 direct freedom ok"\n}',
|
||||
responseSchema: 'LogEntry',
|
||||
responseSchemaArray: true,
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
@@ -966,41 +973,132 @@ export const sections: readonly Section[] = [
|
||||
in: 'query',
|
||||
type: 'number',
|
||||
desc: '1-indexed page number. Defaults to 1.',
|
||||
optional: true,
|
||||
defaultValue: 1,
|
||||
},
|
||||
{
|
||||
name: 'pageSize',
|
||||
in: 'query',
|
||||
type: 'number',
|
||||
desc: 'Rows per page. Defaults to 25, capped at 200.',
|
||||
optional: true,
|
||||
defaultValue: 25,
|
||||
},
|
||||
{
|
||||
name: 'search',
|
||||
in: 'query',
|
||||
type: 'string',
|
||||
desc: 'Case-insensitive substring match on email / subId / comment.',
|
||||
desc: 'Case-insensitive substring match on email, subId, comment, UUID, password, auth or Telegram ID.',
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
name: 'filter',
|
||||
in: 'query',
|
||||
type: 'string',
|
||||
desc: 'Status bucket: online | active | deactive | depleted | expiring.',
|
||||
desc: 'CSV status buckets: online, active, deactive, depleted or expiring. Values are ORed.',
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
name: 'protocol',
|
||||
in: 'query',
|
||||
type: 'string',
|
||||
desc: 'Match clients attached to at least one inbound of this protocol (vless, vmess, trojan, shadowsocks, ...).',
|
||||
desc: 'CSV inbound protocols: vmess, vless, trojan, shadowsocks, wireguard, hysteria, http, mixed, tunnel, tun, mtproto or amneziawg. Values are ORed.',
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
name: 'inbound',
|
||||
in: 'query',
|
||||
type: 'string',
|
||||
desc: 'CSV positive inbound IDs. Values are ORed; invalid or non-positive IDs are ignored.',
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
name: 'sort',
|
||||
in: 'query',
|
||||
type: 'string',
|
||||
desc: 'Sort key: enable | email | inboundIds | traffic | remaining | expiryTime.',
|
||||
desc: 'Sort key. An omitted or unknown value falls back to client ID ascending.',
|
||||
optional: true,
|
||||
enum: [
|
||||
'enable',
|
||||
'email',
|
||||
'inboundIds',
|
||||
'traffic',
|
||||
'remaining',
|
||||
'expiryTime',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'lastOnline',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'order',
|
||||
in: 'query',
|
||||
type: 'string',
|
||||
desc: 'Sort direction. Only descend selects descending order; otherwise ascending.',
|
||||
optional: true,
|
||||
enum: ['ascend', 'descend'],
|
||||
},
|
||||
{
|
||||
name: 'expiryFrom',
|
||||
in: 'query',
|
||||
type: 'number',
|
||||
desc: 'Inclusive minimum expiry time in Unix milliseconds. Zero or negative means unset.',
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
name: 'expiryTo',
|
||||
in: 'query',
|
||||
type: 'number',
|
||||
desc: 'Inclusive maximum expiry time in Unix milliseconds. Zero or negative means unbounded.',
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
name: 'usageFrom',
|
||||
in: 'query',
|
||||
type: 'number',
|
||||
desc: 'Inclusive minimum combined upload and download usage in bytes. Zero means unset.',
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
name: 'usageTo',
|
||||
in: 'query',
|
||||
type: 'number',
|
||||
desc: 'Inclusive maximum combined upload and download usage in bytes. Zero means unbounded.',
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
name: 'autoRenew',
|
||||
in: 'query',
|
||||
type: 'string',
|
||||
desc: 'on selects clients with an interval or calendar-day reset; off selects clients without either.',
|
||||
optional: true,
|
||||
enum: ['on', 'off'],
|
||||
},
|
||||
{
|
||||
name: 'hasTgId',
|
||||
in: 'query',
|
||||
type: 'string',
|
||||
desc: 'yes selects clients with a non-zero Telegram ID; no selects clients without one.',
|
||||
optional: true,
|
||||
enum: ['yes', 'no'],
|
||||
},
|
||||
{
|
||||
name: 'hasComment',
|
||||
in: 'query',
|
||||
type: 'string',
|
||||
desc: 'yes selects clients with a non-blank comment; no selects clients without one.',
|
||||
optional: true,
|
||||
enum: ['yes', 'no'],
|
||||
},
|
||||
{
|
||||
name: 'group',
|
||||
in: 'query',
|
||||
type: 'string',
|
||||
desc: 'CSV group names, matched case-insensitively after trimming. Values are ORed.',
|
||||
optional: true,
|
||||
},
|
||||
{ name: 'order', in: 'query', type: 'string', desc: 'ascend or descend.' },
|
||||
],
|
||||
response:
|
||||
'{\n "success": true,\n "obj": {\n "items": [\n {\n "email": "alice@example.com",\n "subId": "abcd1234",\n "enable": true,\n "totalGB": 53687091200,\n "expiryTime": 1735689600000,\n "limitIp": 0,\n "limitHwid": 0,\n "reset": 0,\n "inboundIds": [3, 5],\n "traffic": { "up": 1024, "down": 4096, "enable": true },\n "createdAt": 1735000000000,\n "updatedAt": 1735100000000\n }\n ],\n "total": 2000,\n "filtered": 47,\n "page": 1,\n "pageSize": 25,\n "summary": {\n "total": 2000,\n "active": 1850,\n "onlineCount": 1,\n "depletedCount": 0,\n "expiringCount": 0,\n "deactiveCount": 150,\n "online": ["alice@example.com"],\n "depleted": [],\n "expiring": [],\n "deactive": ["bob@example.com"]\n }\n }\n}',
|
||||
responseSchema: 'ClientPageResponse',
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
@@ -2503,6 +2601,14 @@ export const sections: readonly Section[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'HEAD',
|
||||
path: '/{subPath}:subid',
|
||||
summary:
|
||||
'Return the same status and subscription metadata headers as GET without a response body.',
|
||||
params: [{ name: 'subid', in: 'path', type: 'string', desc: 'Client subscription ID.' }],
|
||||
responses: subscriptionHeadResponses,
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/{jsonPath}:subid',
|
||||
@@ -2510,6 +2616,14 @@ export const sections: readonly Section[] = [
|
||||
'Return subscription as a JSON array of proxy configs (one per enabled client). Only when JSON subscription is enabled in settings. The path prefix is configured by subJsonPath.',
|
||||
params: [{ name: 'subid', in: 'path', type: 'string', desc: 'Client subscription ID.' }],
|
||||
},
|
||||
{
|
||||
method: 'HEAD',
|
||||
path: '/{jsonPath}:subid',
|
||||
summary:
|
||||
'Return the JSON subscription status and metadata headers without a body. Registered only when JSON subscriptions are enabled.',
|
||||
params: [{ name: 'subid', in: 'path', type: 'string', desc: 'Client subscription ID.' }],
|
||||
responses: subscriptionHeadResponses,
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/{clashPath}:subid',
|
||||
@@ -2517,6 +2631,14 @@ export const sections: readonly Section[] = [
|
||||
'Return subscription as a Clash/Mihomo-compatible YAML config, including configured global Clash routing rules. Only when Clash subscription is enabled in settings. The path prefix is configured by subClashPath.',
|
||||
params: [{ name: 'subid', in: 'path', type: 'string', desc: 'Client subscription ID.' }],
|
||||
},
|
||||
{
|
||||
method: 'HEAD',
|
||||
path: '/{clashPath}:subid',
|
||||
summary:
|
||||
'Return the Clash subscription status and metadata headers without a body. Registered only when Clash subscriptions are enabled.',
|
||||
params: [{ name: 'subid', in: 'path', type: 'string', desc: 'Client subscription ID.' }],
|
||||
responses: subscriptionHeadResponses,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -2531,36 +2653,11 @@ export const sections: readonly Section[] = [
|
||||
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.',
|
||||
},
|
||||
{
|
||||
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 <code>GET /panel/api/server/status</code>.',
|
||||
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.',
|
||||
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}',
|
||||
},
|
||||
{
|
||||
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).',
|
||||
response: '{\n "type": "invalidate",\n "resource": "inbounds"\n}',
|
||||
responses: {
|
||||
'101': { description: 'Switching Protocols. WebSocket messages use WebSocketEnvelope.' },
|
||||
'401': { description: 'No authenticated panel session cookie.' },
|
||||
},
|
||||
security: [{ cookieAuth: [] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
export interface WebSocketEventDoc {
|
||||
type: string;
|
||||
summary: string;
|
||||
payloadSchema: Record<string, unknown>;
|
||||
example: {
|
||||
type: string;
|
||||
payload: unknown;
|
||||
time: number;
|
||||
};
|
||||
}
|
||||
|
||||
const eventTypes = [
|
||||
'status',
|
||||
'traffic',
|
||||
'client_stats',
|
||||
'inbounds',
|
||||
'outbounds',
|
||||
'nodes',
|
||||
'notification',
|
||||
'xray_state',
|
||||
'invalidate',
|
||||
] as const;
|
||||
|
||||
const timestamp = 1735689600000;
|
||||
const int64 = { type: 'integer', format: 'int64' };
|
||||
const stringArray = { type: 'array', items: { type: 'string' } };
|
||||
const stringArrayMap = { type: 'object', additionalProperties: stringArray };
|
||||
const timestampMap = { type: 'object', additionalProperties: int64 };
|
||||
const currentTotal = {
|
||||
type: 'object',
|
||||
required: ['current', 'total'],
|
||||
properties: { current: int64, total: int64 },
|
||||
};
|
||||
|
||||
const statusPayloadSchema = {
|
||||
type: 'object',
|
||||
required: [
|
||||
'cpu',
|
||||
'cpuCores',
|
||||
'logicalPro',
|
||||
'cpuSpeedMhz',
|
||||
'mem',
|
||||
'swap',
|
||||
'disk',
|
||||
'diskIO',
|
||||
'diskTraffic',
|
||||
'xray',
|
||||
'amneziawg',
|
||||
'panelVersion',
|
||||
'panelGuid',
|
||||
'uptime',
|
||||
'loads',
|
||||
'tcpCount',
|
||||
'udpCount',
|
||||
'netIO',
|
||||
'netTraffic',
|
||||
'publicIP',
|
||||
'appStats',
|
||||
],
|
||||
properties: {
|
||||
cpu: { type: 'number' },
|
||||
cpuCores: { type: 'integer' },
|
||||
logicalPro: { type: 'integer' },
|
||||
cpuSpeedMhz: { type: 'number' },
|
||||
mem: currentTotal,
|
||||
swap: currentTotal,
|
||||
disk: currentTotal,
|
||||
diskIO: {
|
||||
type: 'object',
|
||||
required: ['read', 'write'],
|
||||
properties: { read: int64, write: int64 },
|
||||
},
|
||||
diskTraffic: {
|
||||
type: 'object',
|
||||
required: ['read', 'write'],
|
||||
properties: { read: int64, write: int64 },
|
||||
},
|
||||
xray: {
|
||||
type: 'object',
|
||||
required: ['state', 'errorMsg', 'version'],
|
||||
properties: {
|
||||
state: { type: 'string', enum: ['running', 'stop', 'error'] },
|
||||
errorMsg: { type: 'string' },
|
||||
version: { type: 'string' },
|
||||
},
|
||||
},
|
||||
amneziawg: {
|
||||
type: 'object',
|
||||
required: ['configured', 'running'],
|
||||
properties: { configured: { type: 'boolean' }, running: { type: 'boolean' } },
|
||||
},
|
||||
panelVersion: { type: 'string' },
|
||||
panelGuid: { type: 'string' },
|
||||
uptime: int64,
|
||||
loads: { type: 'array', nullable: true, items: { type: 'number' } },
|
||||
tcpCount: { type: 'integer' },
|
||||
udpCount: { type: 'integer' },
|
||||
netIO: {
|
||||
type: 'object',
|
||||
required: ['up', 'down', 'pktUp', 'pktDown'],
|
||||
properties: { up: int64, down: int64, pktUp: int64, pktDown: int64 },
|
||||
},
|
||||
netTraffic: {
|
||||
type: 'object',
|
||||
required: ['sent', 'recv', 'pktSent', 'pktRecv'],
|
||||
properties: { sent: int64, recv: int64, pktSent: int64, pktRecv: int64 },
|
||||
},
|
||||
publicIP: {
|
||||
type: 'object',
|
||||
required: ['ipv4', 'ipv6'],
|
||||
properties: { ipv4: { type: 'string' }, ipv6: { type: 'string' } },
|
||||
},
|
||||
appStats: {
|
||||
type: 'object',
|
||||
required: ['threads', 'mem', 'uptime'],
|
||||
properties: { threads: { type: 'integer' }, mem: int64, uptime: int64 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const trafficPayloadSchema = {
|
||||
type: 'object',
|
||||
required: ['onlineClients', 'onlineByGuid', 'activeInbounds', 'lastOnlineMap'],
|
||||
properties: {
|
||||
traffics: { type: 'array', items: { $ref: '#/components/schemas/Traffic' } },
|
||||
clientTraffics: { type: 'array', items: { $ref: '#/components/schemas/ClientTraffic' } },
|
||||
nodeTraffics: {
|
||||
type: 'array',
|
||||
nullable: true,
|
||||
items: { $ref: '#/components/schemas/Traffic' },
|
||||
},
|
||||
onlineClients: stringArray,
|
||||
onlineByGuid: stringArrayMap,
|
||||
activeInbounds: stringArrayMap,
|
||||
lastOnlineMap: timestampMap,
|
||||
},
|
||||
oneOf: [{ required: ['traffics', 'clientTraffics'] }, { required: ['nodeTraffics'] }],
|
||||
};
|
||||
|
||||
const clientStatsPayloadSchema = {
|
||||
type: 'object',
|
||||
required: ['snapshot'],
|
||||
properties: {
|
||||
snapshot: { type: 'boolean' },
|
||||
clients: { type: 'array', items: { $ref: '#/components/schemas/ClientTraffic' } },
|
||||
inbounds: {
|
||||
type: 'array',
|
||||
items: { $ref: '#/components/schemas/InboundTrafficSummary' },
|
||||
},
|
||||
},
|
||||
anyOf: [{ required: ['clients'] }, { required: ['inbounds'] }],
|
||||
};
|
||||
|
||||
export const websocketEnvelopeSchema = {
|
||||
type: 'object',
|
||||
required: ['type', 'payload', 'time'],
|
||||
properties: {
|
||||
type: { type: 'string', enum: eventTypes },
|
||||
payload: { description: 'Shape is selected by type; see x-websocket-events on GET /ws.' },
|
||||
time: {
|
||||
type: 'integer',
|
||||
format: 'int64',
|
||||
description: 'Server emission time in Unix milliseconds.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function buildWebSocketEvents(
|
||||
examples: Record<string, unknown>,
|
||||
): readonly WebSocketEventDoc[] {
|
||||
return [
|
||||
{
|
||||
type: 'status',
|
||||
summary:
|
||||
'Server health snapshot pushed every two seconds; same payload as server/status obj.',
|
||||
payloadSchema: statusPayloadSchema,
|
||||
example: {
|
||||
type: 'status',
|
||||
payload: {
|
||||
cpu: 12.5,
|
||||
cpuCores: 4,
|
||||
logicalPro: 8,
|
||||
cpuSpeedMhz: 3200,
|
||||
mem: { current: 2147483648, total: 8589934592 },
|
||||
swap: { current: 0, total: 2147483648 },
|
||||
disk: { current: 53687091200, total: 107374182400 },
|
||||
diskIO: { read: 1048576, write: 2097152 },
|
||||
diskTraffic: { read: 4096, write: 8192 },
|
||||
xray: { state: 'running', errorMsg: '', version: '25.10.31' },
|
||||
amneziawg: { configured: false, running: false },
|
||||
panelVersion: 'v3.x.x',
|
||||
panelGuid: 'panel-guid',
|
||||
uptime: 86400,
|
||||
loads: [0.1, 0.2, 0.3],
|
||||
tcpCount: 24,
|
||||
udpCount: 8,
|
||||
netIO: { up: 1048576, down: 2097152, pktUp: 100, pktDown: 200 },
|
||||
netTraffic: { sent: 4096, recv: 8192, pktSent: 10, pktRecv: 20 },
|
||||
publicIP: { ipv4: '192.0.2.1', ipv6: '2001:db8::1' },
|
||||
appStats: { threads: 16, mem: 67108864, uptime: 3600 },
|
||||
},
|
||||
time: timestamp,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'traffic',
|
||||
summary:
|
||||
'Live traffic deltas plus online, per-node and last-online maps. Local polls send traffics/clientTraffics; node polls send nodeTraffics.',
|
||||
payloadSchema: trafficPayloadSchema,
|
||||
example: {
|
||||
type: 'traffic',
|
||||
payload: {
|
||||
traffics: [examples.Traffic],
|
||||
clientTraffics: [examples.ClientTraffic],
|
||||
onlineClients: ['alice@example.com'],
|
||||
onlineByGuid: { 'panel-guid': ['alice@example.com'] },
|
||||
activeInbounds: { 'panel-guid': ['inbound-443'] },
|
||||
lastOnlineMap: { 'alice@example.com': timestamp },
|
||||
},
|
||||
time: timestamp,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'client_stats',
|
||||
summary:
|
||||
'Absolute client counters and/or inbound summaries; snapshot says whether clients is complete or only recently active rows.',
|
||||
payloadSchema: clientStatsPayloadSchema,
|
||||
example: {
|
||||
type: 'client_stats',
|
||||
payload: {
|
||||
snapshot: true,
|
||||
clients: [examples.ClientTraffic],
|
||||
inbounds: [examples.InboundTrafficSummary],
|
||||
},
|
||||
time: timestamp,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'inbounds',
|
||||
summary: 'Full inbound list after an inbound mutation, unless invalidate is used at scale.',
|
||||
payloadSchema: { type: 'array', items: { $ref: '#/components/schemas/Inbound' } },
|
||||
example: { type: 'inbounds', payload: [examples.Inbound], time: timestamp },
|
||||
},
|
||||
{
|
||||
type: 'outbounds',
|
||||
summary: 'Current outbound traffic rows after the periodic traffic collection.',
|
||||
payloadSchema: {
|
||||
type: 'array',
|
||||
items: { $ref: '#/components/schemas/OutboundTraffics' },
|
||||
},
|
||||
example: { type: 'outbounds', payload: [examples.OutboundTraffics], time: timestamp },
|
||||
},
|
||||
{
|
||||
type: 'nodes',
|
||||
summary: 'Current node tree after the heartbeat probe cycle.',
|
||||
payloadSchema: { type: 'array', items: { $ref: '#/components/schemas/NodeView' } },
|
||||
example: { type: 'nodes', payload: [examples.NodeView], time: timestamp },
|
||||
},
|
||||
{
|
||||
type: 'notification',
|
||||
summary: 'An in-panel notification emitted by server actions.',
|
||||
payloadSchema: {
|
||||
type: 'object',
|
||||
required: ['title', 'message', 'level'],
|
||||
properties: {
|
||||
title: { type: 'string' },
|
||||
message: { type: 'string' },
|
||||
level: { type: 'string', enum: ['success', 'warning'] },
|
||||
},
|
||||
},
|
||||
example: {
|
||||
type: 'notification',
|
||||
payload: {
|
||||
title: 'Xray service restarted',
|
||||
message: 'Xray service has been restarted successfully',
|
||||
level: 'success',
|
||||
},
|
||||
time: timestamp,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'xray_state',
|
||||
summary: 'Xray process state change after a stop, restart or error.',
|
||||
payloadSchema: {
|
||||
type: 'object',
|
||||
required: ['state', 'errorMsg'],
|
||||
properties: {
|
||||
state: { type: 'string', enum: ['running', 'stop', 'error'] },
|
||||
errorMsg: { type: 'string' },
|
||||
},
|
||||
},
|
||||
example: {
|
||||
type: 'xray_state',
|
||||
payload: { state: 'running', errorMsg: '' },
|
||||
time: timestamp,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'invalidate',
|
||||
summary:
|
||||
'Requests a REST re-fetch. clients is an invalidate payload type, not a top-level event.',
|
||||
payloadSchema: {
|
||||
type: 'object',
|
||||
required: ['type'],
|
||||
properties: {
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'status',
|
||||
'traffic',
|
||||
'client_stats',
|
||||
'inbounds',
|
||||
'outbounds',
|
||||
'nodes',
|
||||
'notification',
|
||||
'xray_state',
|
||||
'clients',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
example: { type: 'invalidate', payload: { type: 'inbounds' }, time: timestamp },
|
||||
},
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user