1
0

websocket-events.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. export interface WebSocketEventDoc {
  2. type: string;
  3. summary: string;
  4. payloadSchema: Record<string, unknown>;
  5. example: {
  6. type: string;
  7. payload: unknown;
  8. time: number;
  9. };
  10. }
  11. const eventTypes = [
  12. 'status',
  13. 'traffic',
  14. 'client_stats',
  15. 'inbounds',
  16. 'outbounds',
  17. 'nodes',
  18. 'notification',
  19. 'xray_state',
  20. 'invalidate',
  21. ] as const;
  22. const timestamp = 1735689600000;
  23. const int64 = { type: 'integer', format: 'int64' };
  24. const stringArray = { type: 'array', items: { type: 'string' } };
  25. const stringArrayMap = { type: 'object', additionalProperties: stringArray };
  26. const timestampMap = { type: 'object', additionalProperties: int64 };
  27. const currentTotal = {
  28. type: 'object',
  29. required: ['current', 'total'],
  30. properties: { current: int64, total: int64 },
  31. };
  32. const statusPayloadSchema = {
  33. type: 'object',
  34. required: [
  35. 'cpu',
  36. 'cpuCores',
  37. 'logicalPro',
  38. 'cpuSpeedMhz',
  39. 'mem',
  40. 'swap',
  41. 'disk',
  42. 'diskIO',
  43. 'diskTraffic',
  44. 'xray',
  45. 'amneziawg',
  46. 'panelVersion',
  47. 'panelGuid',
  48. 'uptime',
  49. 'loads',
  50. 'tcpCount',
  51. 'udpCount',
  52. 'netIO',
  53. 'netTraffic',
  54. 'publicIP',
  55. 'appStats',
  56. ],
  57. properties: {
  58. cpu: { type: 'number' },
  59. cpuCores: { type: 'integer' },
  60. logicalPro: { type: 'integer' },
  61. cpuSpeedMhz: { type: 'number' },
  62. mem: currentTotal,
  63. swap: currentTotal,
  64. disk: currentTotal,
  65. diskIO: {
  66. type: 'object',
  67. required: ['read', 'write'],
  68. properties: { read: int64, write: int64 },
  69. },
  70. diskTraffic: {
  71. type: 'object',
  72. required: ['read', 'write'],
  73. properties: { read: int64, write: int64 },
  74. },
  75. xray: {
  76. type: 'object',
  77. required: ['state', 'errorMsg', 'version'],
  78. properties: {
  79. state: { type: 'string', enum: ['running', 'stop', 'error'] },
  80. errorMsg: { type: 'string' },
  81. version: { type: 'string' },
  82. },
  83. },
  84. amneziawg: {
  85. type: 'object',
  86. required: ['configured', 'running'],
  87. properties: { configured: { type: 'boolean' }, running: { type: 'boolean' } },
  88. },
  89. panelVersion: { type: 'string' },
  90. panelGuid: { type: 'string' },
  91. uptime: int64,
  92. loads: { type: 'array', nullable: true, items: { type: 'number' } },
  93. tcpCount: { type: 'integer' },
  94. udpCount: { type: 'integer' },
  95. netIO: {
  96. type: 'object',
  97. required: ['up', 'down', 'pktUp', 'pktDown'],
  98. properties: { up: int64, down: int64, pktUp: int64, pktDown: int64 },
  99. },
  100. netTraffic: {
  101. type: 'object',
  102. required: ['sent', 'recv', 'pktSent', 'pktRecv'],
  103. properties: { sent: int64, recv: int64, pktSent: int64, pktRecv: int64 },
  104. },
  105. publicIP: {
  106. type: 'object',
  107. required: ['ipv4', 'ipv6'],
  108. properties: { ipv4: { type: 'string' }, ipv6: { type: 'string' } },
  109. },
  110. appStats: {
  111. type: 'object',
  112. required: ['threads', 'mem', 'uptime'],
  113. properties: { threads: { type: 'integer' }, mem: int64, uptime: int64 },
  114. },
  115. },
  116. };
  117. const trafficPayloadSchema = {
  118. type: 'object',
  119. properties: {
  120. traffics: { type: 'array', items: { $ref: '#/components/schemas/Traffic' } },
  121. clientTraffics: { type: 'array', items: { $ref: '#/components/schemas/ClientTraffic' } },
  122. clientTrafficSource: {
  123. type: 'string',
  124. enum: ['xray', 'tuic'],
  125. description: 'Present for native TUIC samples; omitted Xray samples default to xray.',
  126. },
  127. clientTrafficIntervalMs: {
  128. type: 'integer',
  129. description: 'Sampling interval used to calculate client speed, in milliseconds.',
  130. },
  131. nodeTraffics: {
  132. type: 'array',
  133. nullable: true,
  134. items: { $ref: '#/components/schemas/Traffic' },
  135. },
  136. onlineClients: stringArray,
  137. onlineByGuid: stringArrayMap,
  138. activeInbounds: stringArrayMap,
  139. lastOnlineMap: timestampMap,
  140. },
  141. oneOf: [
  142. {
  143. required: [
  144. 'traffics',
  145. 'clientTraffics',
  146. 'onlineClients',
  147. 'onlineByGuid',
  148. 'activeInbounds',
  149. 'lastOnlineMap',
  150. ],
  151. },
  152. { required: ['nodeTraffics'] },
  153. { required: ['clientTraffics', 'clientTrafficSource', 'clientTrafficIntervalMs'] },
  154. ],
  155. };
  156. const clientStatsPayloadSchema = {
  157. type: 'object',
  158. required: ['snapshot'],
  159. properties: {
  160. snapshot: { type: 'boolean' },
  161. clients: { type: 'array', items: { $ref: '#/components/schemas/ClientTraffic' } },
  162. inbounds: {
  163. type: 'array',
  164. items: { $ref: '#/components/schemas/InboundTrafficSummary' },
  165. },
  166. },
  167. anyOf: [{ required: ['clients'] }, { required: ['inbounds'] }],
  168. };
  169. export const websocketEnvelopeSchema = {
  170. type: 'object',
  171. required: ['type', 'payload', 'time'],
  172. properties: {
  173. type: { type: 'string', enum: eventTypes },
  174. payload: { description: 'Shape is selected by type; see x-websocket-events on GET /ws.' },
  175. time: {
  176. type: 'integer',
  177. format: 'int64',
  178. description: 'Server emission time in Unix milliseconds.',
  179. },
  180. },
  181. };
  182. export function buildWebSocketEvents(
  183. examples: Record<string, unknown>,
  184. ): readonly WebSocketEventDoc[] {
  185. return [
  186. {
  187. type: 'status',
  188. summary:
  189. 'Server health snapshot pushed every two seconds; same payload as server/status obj.',
  190. payloadSchema: statusPayloadSchema,
  191. example: {
  192. type: 'status',
  193. payload: {
  194. cpu: 12.5,
  195. cpuCores: 4,
  196. logicalPro: 8,
  197. cpuSpeedMhz: 3200,
  198. mem: { current: 2147483648, total: 8589934592 },
  199. swap: { current: 0, total: 2147483648 },
  200. disk: { current: 53687091200, total: 107374182400 },
  201. diskIO: { read: 1048576, write: 2097152 },
  202. diskTraffic: { read: 4096, write: 8192 },
  203. xray: { state: 'running', errorMsg: '', version: '25.10.31' },
  204. amneziawg: { configured: false, running: false },
  205. panelVersion: 'v3.x.x',
  206. panelGuid: 'panel-guid',
  207. uptime: 86400,
  208. loads: [0.1, 0.2, 0.3],
  209. tcpCount: 24,
  210. udpCount: 8,
  211. netIO: { up: 1048576, down: 2097152, pktUp: 100, pktDown: 200 },
  212. netTraffic: { sent: 4096, recv: 8192, pktSent: 10, pktRecv: 20 },
  213. publicIP: { ipv4: '192.0.2.1', ipv6: '2001:db8::1' },
  214. appStats: { threads: 16, mem: 67108864, uptime: 3600 },
  215. },
  216. time: timestamp,
  217. },
  218. },
  219. {
  220. type: 'traffic',
  221. summary:
  222. 'Live traffic deltas plus online, per-node and last-online maps. TUIC also sends source-tagged client deltas with their sampling interval for live speed.',
  223. payloadSchema: trafficPayloadSchema,
  224. example: {
  225. type: 'traffic',
  226. payload: {
  227. traffics: [examples.Traffic],
  228. clientTraffics: [examples.ClientTraffic],
  229. onlineClients: ['[email protected]'],
  230. onlineByGuid: { 'panel-guid': ['[email protected]'] },
  231. activeInbounds: { 'panel-guid': ['inbound-443'] },
  232. lastOnlineMap: { '[email protected]': timestamp },
  233. },
  234. time: timestamp,
  235. },
  236. },
  237. {
  238. type: 'client_stats',
  239. summary:
  240. 'Absolute client counters and/or inbound summaries; snapshot says whether clients is complete or only recently active rows.',
  241. payloadSchema: clientStatsPayloadSchema,
  242. example: {
  243. type: 'client_stats',
  244. payload: {
  245. snapshot: true,
  246. clients: [examples.ClientTraffic],
  247. inbounds: [examples.InboundTrafficSummary],
  248. },
  249. time: timestamp,
  250. },
  251. },
  252. {
  253. type: 'inbounds',
  254. summary: 'Full inbound list after an inbound mutation, unless invalidate is used at scale.',
  255. payloadSchema: { type: 'array', items: { $ref: '#/components/schemas/Inbound' } },
  256. example: { type: 'inbounds', payload: [examples.Inbound], time: timestamp },
  257. },
  258. {
  259. type: 'outbounds',
  260. summary: 'Current outbound traffic rows after the periodic traffic collection.',
  261. payloadSchema: {
  262. type: 'array',
  263. items: { $ref: '#/components/schemas/OutboundTraffics' },
  264. },
  265. example: { type: 'outbounds', payload: [examples.OutboundTraffics], time: timestamp },
  266. },
  267. {
  268. type: 'nodes',
  269. summary: 'Current node tree after the heartbeat probe cycle.',
  270. payloadSchema: { type: 'array', items: { $ref: '#/components/schemas/NodeView' } },
  271. example: { type: 'nodes', payload: [examples.NodeView], time: timestamp },
  272. },
  273. {
  274. type: 'notification',
  275. summary: 'An in-panel notification emitted by server actions.',
  276. payloadSchema: {
  277. type: 'object',
  278. required: ['title', 'message', 'level'],
  279. properties: {
  280. title: { type: 'string' },
  281. message: { type: 'string' },
  282. level: { type: 'string', enum: ['success', 'warning'] },
  283. },
  284. },
  285. example: {
  286. type: 'notification',
  287. payload: {
  288. title: 'Xray service restarted',
  289. message: 'Xray service has been restarted successfully',
  290. level: 'success',
  291. },
  292. time: timestamp,
  293. },
  294. },
  295. {
  296. type: 'xray_state',
  297. summary: 'Xray process state change after a stop, restart or error.',
  298. payloadSchema: {
  299. type: 'object',
  300. required: ['state', 'errorMsg'],
  301. properties: {
  302. state: { type: 'string', enum: ['running', 'stop', 'error'] },
  303. errorMsg: { type: 'string' },
  304. },
  305. },
  306. example: {
  307. type: 'xray_state',
  308. payload: { state: 'running', errorMsg: '' },
  309. time: timestamp,
  310. },
  311. },
  312. {
  313. type: 'invalidate',
  314. summary:
  315. 'Requests a REST re-fetch. clients is an invalidate payload type, not a top-level event.',
  316. payloadSchema: {
  317. type: 'object',
  318. required: ['type'],
  319. properties: {
  320. type: {
  321. type: 'string',
  322. enum: [
  323. 'status',
  324. 'traffic',
  325. 'client_stats',
  326. 'inbounds',
  327. 'outbounds',
  328. 'nodes',
  329. 'notification',
  330. 'xray_state',
  331. 'clients',
  332. ],
  333. },
  334. },
  335. },
  336. example: { type: 'invalidate', payload: { type: 'inbounds' }, time: timestamp },
  337. },
  338. ];
  339. }