NodeView carried only the category counts, so the nodes page chips could hover to nothing but the category title. GetAll/recountByGuid now emit online/active/disabled/depleted email lists from the same bucketing pass as the counts (statuses gain the client email; the online list is the counted set), and the chips pop the list over with the inbounds page's popover pattern.
142 lines
5.8 KiB
TypeScript
142 lines
5.8 KiB
TypeScript
import { z } from 'zod';
|
|
|
|
export const NodeRecordSchema = z
|
|
.object({
|
|
id: z.number(),
|
|
name: z.string().optional(),
|
|
remark: z.string().optional(),
|
|
scheme: z.string().optional(),
|
|
address: z.string().optional(),
|
|
port: z.number().optional(),
|
|
basePath: z.string().optional(),
|
|
apiToken: z.string().optional(),
|
|
hasApiToken: z.boolean().optional(),
|
|
enable: z.boolean().optional(),
|
|
status: z.string().optional(),
|
|
latencyMs: z.number().optional(),
|
|
cpuPct: z.number().optional(),
|
|
memPct: z.number().optional(),
|
|
xrayVersion: z.string().optional(),
|
|
panelVersion: z.string().optional(),
|
|
uptimeSecs: z.number().optional(),
|
|
inboundCount: z.number().optional(),
|
|
clientCount: z.number().optional(),
|
|
onlineCount: z.number().optional(),
|
|
activeCount: z.number().optional(),
|
|
disabledCount: z.number().optional(),
|
|
depletedCount: z.number().optional(),
|
|
// Per-category client email lists behind the counts above; the nodes
|
|
// page category chips pop these over on hover.
|
|
onlineEmails: z.array(z.string()).optional(),
|
|
activeEmails: z.array(z.string()).optional(),
|
|
disabledEmails: z.array(z.string()).optional(),
|
|
depletedEmails: z.array(z.string()).optional(),
|
|
lastHeartbeat: z.number().optional(),
|
|
lastError: z.string().optional(),
|
|
// Xray state captured from the remote node's own /panel/api/server/status.
|
|
// Lets the nodes list show a distinct indicator when the panel API is reachable
|
|
// (status=online) but the Xray core on that node has failed.
|
|
xrayState: z.string().optional(),
|
|
xrayError: z.string().optional(),
|
|
allowPrivateAddress: z.boolean().optional(),
|
|
tlsVerifyMode: z.enum(['verify', 'skip', 'pin', 'mtls']).optional(),
|
|
pinnedCertSha256: z.string().optional(),
|
|
inboundSyncMode: z.enum(['all', 'selected']).optional(),
|
|
// Backend serializes a nil []string as null for nodes saved before #5178.
|
|
inboundTags: z.array(z.string()).nullish(),
|
|
outboundTag: z.string().optional(),
|
|
// Multi-hop node tree (#4983): a node's stable GUID, its parent's GUID, and
|
|
// whether it's a read-only transitive sub-node surfaced from a downstream node.
|
|
guid: z.string().optional(),
|
|
parentGuid: z.string().optional(),
|
|
transitive: z.boolean().optional(),
|
|
// P7: node kind, annotated by the backend for nodes registered by an
|
|
// agent. Absent/traditional for classic panel-to-panel nodes.
|
|
nodeType: z.enum(['traditional', 'agent']).optional(),
|
|
// P13: host IPv4/IPv6 an agent reports with its heartbeat telemetry.
|
|
// Rendered as plain text in the address column for agent rows.
|
|
ipv4: z.string().optional(),
|
|
ipv6: z.string().optional(),
|
|
// Item 20: last one-click agent update outcome, agent rows only.
|
|
agentUpdateState: z.string().optional(),
|
|
agentUpdateError: z.string().optional(),
|
|
agentUpdateTarget: z.string().optional(),
|
|
})
|
|
.loose();
|
|
|
|
export const NodeListSchema = z.array(NodeRecordSchema);
|
|
|
|
export const ProbeResultSchema = z
|
|
.object({
|
|
status: z.string(),
|
|
latencyMs: z.number().optional(),
|
|
xrayVersion: z.string().optional(),
|
|
error: z.string().optional(),
|
|
// Present on successful probe; used to surface "connected to panel, but xray failed on node".
|
|
xrayState: z.string().optional(),
|
|
xrayError: z.string().optional(),
|
|
})
|
|
.loose();
|
|
|
|
export const NodeFormSchema = z
|
|
.object({
|
|
id: z.number().optional(),
|
|
name: z.string().trim().min(1, 'pages.nodes.toasts.fillRequired'),
|
|
remark: z.string().optional(),
|
|
// Node type: traditional panel-to-panel, or lightweight agent.
|
|
// Agent nodes skip the panel probe and API token; an agent token is
|
|
// issued on creation instead.
|
|
nodeType: z.enum(['traditional', 'agent']).optional().default('traditional'),
|
|
scheme: z.enum(['http', 'https']),
|
|
// For agent nodes the panel never dials the node (agent dials out),
|
|
// so address/port are not needed. Required for traditional nodes.
|
|
address: z.string().trim().optional().default(''),
|
|
port: z.number().int().min(1).max(65535).optional(),
|
|
basePath: z.string(),
|
|
// mTLS nodes authenticate via the client certificate, so the token is optional
|
|
// there; every other verify mode still requires one (matches remote.do()).
|
|
// Agent nodes use an agent token instead of a panel API token.
|
|
apiToken: z.string().trim(),
|
|
hasStoredToken: z.boolean().optional().default(false),
|
|
enable: z.boolean(),
|
|
allowPrivateAddress: z.boolean(),
|
|
tlsVerifyMode: z.enum(['verify', 'skip', 'pin', 'mtls']),
|
|
pinnedCertSha256: z.string().optional().default(''),
|
|
inboundSyncMode: z.enum(['all', 'selected']).optional().default('all'),
|
|
// Unmounted when sync mode is "all" (absent from antd onFinish values) and
|
|
// serialized as null by the backend for a nil slice — tolerate both.
|
|
inboundTags: z
|
|
.array(z.string())
|
|
.nullish()
|
|
.transform((tags) => tags ?? []),
|
|
outboundTag: z.string().optional(),
|
|
})
|
|
.superRefine((val, ctx) => {
|
|
if (val.nodeType === 'agent') return; // no panel credentials needed
|
|
if (!val.address || val.address.trim().length === 0) {
|
|
ctx.addIssue({
|
|
code: 'custom',
|
|
path: ['address'],
|
|
message: 'pages.nodes.toasts.fillRequired',
|
|
});
|
|
}
|
|
if (val.port === undefined) {
|
|
ctx.addIssue({
|
|
code: 'custom',
|
|
path: ['port'],
|
|
message: 'pages.nodes.toasts.fillRequired',
|
|
});
|
|
}
|
|
if (val.tlsVerifyMode !== 'mtls' && val.apiToken.length === 0 && !val.hasStoredToken) {
|
|
ctx.addIssue({
|
|
code: 'custom',
|
|
path: ['apiToken'],
|
|
message: 'pages.nodes.toasts.fillRequired',
|
|
});
|
|
}
|
|
});
|
|
|
|
export type NodeRecord = z.infer<typeof NodeRecordSchema>;
|
|
export type ProbeResult = z.infer<typeof ProbeResultSchema>;
|
|
export type NodeFormValues = z.infer<typeof NodeFormSchema>;
|