CI / go-test (push) Waiting to run
CI / postgres-durable-first (push) Waiting to run
CI / codegen (push) Waiting to run
CI / govulncheck (push) Waiting to run
CI / race (push) Waiting to run
CI / fuzz-smoke (push) Waiting to run
CI / golangci (push) Waiting to run
CI / frontend (push) Waiting to run
Docs CI / build (push) Waiting to run
Docs Deploy (GitHub Pages) / build (push) Waiting to run
Docs Deploy (GitHub Pages) / deploy (push) Blocked by required conditions
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (amd64) (push) Waiting to run
Release 3X-UI / Publish rolling dev release (push) Blocked by required conditions
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
- 0: sing-box gen overrides fragment listen/port from the inbound row (empty listen -> "::"), Update/Add recompute the stored fragment; non-loopback E2E against the real core binary. - 1: agent fetches the sing-box core from the Gitea release when missing, self-checks it, and only then latches the switch. - 2: node list shows the sing-box core version (heartbeat field, persisted on the node row), xray version as fallback. - 3: public-page fallback returns 404 to anonymous visitors when the page is disabled. - 4: settings restart hint bar removed, dead i18n keys cleaned. - 5: two PUT calls missing JSON headers fixed (audit: only those two), batch probe-task creation endpoint + UI. - 6: cf-probe retirement card and coverage pipeline removed. - 7: generated vless links no longer carry spx (panel export + sub); parser still accepts spx; golden fixtures updated. - probe batch: dedicated probe settings tab (public-page settings, tasks, per-node meta), retention_days editable, per-node traffic calibration + stat-interface pinning, runtime metrics split into a lightweight fast report (default 3s, 1-60s adjustable). - 8: sing-box core processes start with GOMEMLIMIT=32MiB, GOGC=50. - 9: dead /getTwoFactorEnable docs entries removed (frontend call and contract entry already gone at base; verified by grep).
145 lines
5.9 KiB
TypeScript
145 lines
5.9 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(),
|
|
// Agent nodes run sing-box since V2: their core version is the
|
|
// sing-box one the heartbeat reports (V2.0.1 ②).
|
|
singboxVersion: 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>;
|