mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-11 23:26:07 +00:00
feat(nodes): add distinct purple indicator when panel is online but Xray core failed (#5040)
* feat(nodes): add distinct purple indicator when panel is online but Xray core failed Currently nodes only show binary online/offline based on panel API reachability. This adds a third state: - Green: panel reachable + Xray healthy - Purple pulsing dot + "Online (Xray Error)": panel API works (management actions still available) but the node Xray process is in error or stopped. Tooltip shows the remote xrayError. - Red: unreachable (unchanged) Backend now captures xray.state + xray.errorMsg from /panel/api/server/status heartbeats and probes. New fields on Node + NodeSummary, forwarded for transitive nodes. Frontend Zod + NodeList rendering + dedicated .xray-error-dot CSS (color #722ED1) + i18n key. Color chosen purple per feedback after initial implementation. Refs: worktree xray-failed-in-nodes * fix: remove invalid JSON comment causing CI failures * chore: regenerate OpenAPI schemas and types for xray error indicators * chore: regenerate examples and schemas for xray error indicators * chore: regenerate missing openapi.json examples * fix --------- Co-authored-by: Rqzbeh <rqzbeh@users.noreply.github.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
+12
-6
@@ -478,6 +478,12 @@ type Node struct {
|
||||
UptimeSecs uint64 `json:"uptimeSecs" example:"86400"`
|
||||
LastError string `json:"lastError"`
|
||||
|
||||
// XrayState and XrayError are captured from the remote node's /panel/api/server/status
|
||||
// during heartbeats. They let the central panel distinguish "panel API reachable"
|
||||
// (status=online) from "Xray core itself has failed on the node" for monitoring.
|
||||
XrayState string `json:"xrayState" gorm:"column:xray_state"`
|
||||
XrayError string `json:"xrayError" gorm:"column:xray_error"`
|
||||
|
||||
ConfigDirty bool `json:"configDirty" gorm:"default:false"`
|
||||
ConfigDirtyAt int64 `json:"configDirtyAt"`
|
||||
|
||||
@@ -514,6 +520,9 @@ type NodeSummary struct {
|
||||
LatencyMs int `json:"latencyMs"`
|
||||
PanelVersion string `json:"panelVersion"`
|
||||
XrayVersion string `json:"xrayVersion"`
|
||||
// XrayState/XrayError forwarded so masters can surface xray failure on transitive sub-nodes too.
|
||||
XrayState string `json:"xrayState"`
|
||||
XrayError string `json:"xrayError,omitempty"`
|
||||
}
|
||||
|
||||
type CustomGeoResource struct {
|
||||
@@ -713,18 +722,15 @@ type OutboundSubscription struct {
|
||||
AllowPrivate bool `json:"allowPrivate" form:"allowPrivate" gorm:"default:false"`
|
||||
TagPrefix string `json:"tagPrefix" form:"tagPrefix"`
|
||||
UpdateInterval int `json:"updateInterval" form:"updateInterval" gorm:"default:600"` // seconds between refreshes
|
||||
Priority int `json:"priority" form:"priority" gorm:"default:0"` // order among subscriptions in the merged outbounds (lower = earlier)
|
||||
Prepend bool `json:"prepend" form:"prepend" gorm:"default:false"` // place this subscription's outbounds before the manual template outbounds
|
||||
Priority int `json:"priority" form:"priority" gorm:"default:0"` // order among subscriptions in the merged outbounds (lower = earlier)
|
||||
Prepend bool `json:"prepend" form:"prepend" gorm:"default:false"` // place this subscription's outbounds before the manual template outbounds
|
||||
LastUpdated int64 `json:"lastUpdated" form:"lastUpdated"`
|
||||
LastError string `json:"lastError" form:"lastError"`
|
||||
LastFetchedOutbounds string `json:"lastFetchedOutbounds" form:"lastFetchedOutbounds" gorm:"type:text"`
|
||||
LinkIdentities string `json:"-" gorm:"type:text;column:link_identities"`
|
||||
CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
|
||||
UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"`
|
||||
// OutboundCount is a derived count of the last fetched outbounds (not
|
||||
// persisted); List populates it so the UI can show how many outbounds a
|
||||
// subscription produced without shipping the full payload.
|
||||
OutboundCount int `json:"outboundCount" gorm:"-"`
|
||||
OutboundCount int `json:"outboundCount" gorm:"-"`
|
||||
}
|
||||
|
||||
func MergeClientRecord(existing *ClientRecord, incoming *ClientRecord) []ClientMergeConflict {
|
||||
|
||||
@@ -1656,6 +1656,13 @@
|
||||
"example": 86400,
|
||||
"type": "integer"
|
||||
},
|
||||
"xrayError": {
|
||||
"type": "string"
|
||||
},
|
||||
"xrayState": {
|
||||
"description": "XrayState and XrayError are captured from the remote node's /panel/api/server/status\nduring heartbeats. They let the central panel distinguish \"panel API reachable\"\n(status=online) from \"Xray core itself has failed on the node\" for monitoring.",
|
||||
"type": "string"
|
||||
},
|
||||
"xrayVersion": {
|
||||
"example": "25.10.31",
|
||||
"type": "string"
|
||||
@@ -1691,6 +1698,8 @@
|
||||
"tlsVerifyMode",
|
||||
"updatedAt",
|
||||
"uptimeSecs",
|
||||
"xrayError",
|
||||
"xrayState",
|
||||
"xrayVersion"
|
||||
],
|
||||
"type": "object"
|
||||
@@ -1752,6 +1761,13 @@
|
||||
"example": 86400,
|
||||
"type": "integer"
|
||||
},
|
||||
"xrayError": {
|
||||
"type": "string"
|
||||
},
|
||||
"xrayState": {
|
||||
"description": "XrayState/XrayError are populated on successful probes even when the node's\nXray core is not healthy. The UI uses them for a distinct \"panel ok, xray failed\" indicator.",
|
||||
"type": "string"
|
||||
},
|
||||
"xrayVersion": {
|
||||
"example": "25.10.31",
|
||||
"type": "string"
|
||||
@@ -1765,6 +1781,8 @@
|
||||
"panelVersion",
|
||||
"status",
|
||||
"uptimeSecs",
|
||||
"xrayError",
|
||||
"xrayState",
|
||||
"xrayVersion"
|
||||
],
|
||||
"type": "object"
|
||||
@@ -5894,6 +5912,8 @@
|
||||
"transitive": false,
|
||||
"updatedAt": 1700000000,
|
||||
"uptimeSecs": 86400,
|
||||
"xrayError": "",
|
||||
"xrayState": "",
|
||||
"xrayVersion": "25.10.31"
|
||||
}
|
||||
]
|
||||
@@ -6254,6 +6274,8 @@
|
||||
"panelVersion": "v3.x.x",
|
||||
"status": "online",
|
||||
"uptimeSecs": 86400,
|
||||
"xrayError": "",
|
||||
"xrayState": "",
|
||||
"xrayVersion": "25.10.31"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,6 +364,8 @@ export const EXAMPLES: Record<string, unknown> = {
|
||||
"transitive": false,
|
||||
"updatedAt": 1700000000,
|
||||
"uptimeSecs": 86400,
|
||||
"xrayError": "",
|
||||
"xrayState": "",
|
||||
"xrayVersion": "25.10.31"
|
||||
},
|
||||
"OutboundTraffics": {
|
||||
@@ -381,6 +383,8 @@ export const EXAMPLES: Record<string, unknown> = {
|
||||
"panelVersion": "v3.x.x",
|
||||
"status": "online",
|
||||
"uptimeSecs": 86400,
|
||||
"xrayError": "",
|
||||
"xrayState": "",
|
||||
"xrayVersion": "25.10.31"
|
||||
},
|
||||
"Setting": {
|
||||
|
||||
@@ -1630,6 +1630,13 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"example": 86400,
|
||||
"type": "integer"
|
||||
},
|
||||
"xrayError": {
|
||||
"type": "string"
|
||||
},
|
||||
"xrayState": {
|
||||
"description": "XrayState and XrayError are captured from the remote node's /panel/api/server/status\nduring heartbeats. They let the central panel distinguish \"panel API reachable\"\n(status=online) from \"Xray core itself has failed on the node\" for monitoring.",
|
||||
"type": "string"
|
||||
},
|
||||
"xrayVersion": {
|
||||
"example": "25.10.31",
|
||||
"type": "string"
|
||||
@@ -1665,6 +1672,8 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"tlsVerifyMode",
|
||||
"updatedAt",
|
||||
"uptimeSecs",
|
||||
"xrayError",
|
||||
"xrayState",
|
||||
"xrayVersion"
|
||||
],
|
||||
"type": "object"
|
||||
@@ -1726,6 +1735,13 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"example": 86400,
|
||||
"type": "integer"
|
||||
},
|
||||
"xrayError": {
|
||||
"type": "string"
|
||||
},
|
||||
"xrayState": {
|
||||
"description": "XrayState/XrayError are populated on successful probes even when the node's\nXray core is not healthy. The UI uses them for a distinct \"panel ok, xray failed\" indicator.",
|
||||
"type": "string"
|
||||
},
|
||||
"xrayVersion": {
|
||||
"example": "25.10.31",
|
||||
"type": "string"
|
||||
@@ -1739,6 +1755,8 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"panelVersion",
|
||||
"status",
|
||||
"uptimeSecs",
|
||||
"xrayError",
|
||||
"xrayState",
|
||||
"xrayVersion"
|
||||
],
|
||||
"type": "object"
|
||||
|
||||
@@ -371,6 +371,8 @@ export interface Node {
|
||||
transitive?: boolean;
|
||||
updatedAt: number;
|
||||
uptimeSecs: number;
|
||||
xrayError: string;
|
||||
xrayState: string;
|
||||
xrayVersion: string;
|
||||
}
|
||||
|
||||
@@ -390,6 +392,8 @@ export interface ProbeResultUI {
|
||||
panelVersion: string;
|
||||
status: string;
|
||||
uptimeSecs: number;
|
||||
xrayError: string;
|
||||
xrayState: string;
|
||||
xrayVersion: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -398,6 +398,8 @@ export const NodeSchema = z.object({
|
||||
transitive: z.boolean().optional(),
|
||||
updatedAt: z.number().int(),
|
||||
uptimeSecs: z.number().int(),
|
||||
xrayError: z.string(),
|
||||
xrayState: z.string(),
|
||||
xrayVersion: z.string(),
|
||||
});
|
||||
export type Node = z.infer<typeof NodeSchema>;
|
||||
@@ -419,6 +421,8 @@ export const ProbeResultUISchema = z.object({
|
||||
panelVersion: z.string(),
|
||||
status: z.string(),
|
||||
uptimeSecs: z.number().int(),
|
||||
xrayError: z.string(),
|
||||
xrayState: z.string(),
|
||||
xrayVersion: z.string(),
|
||||
});
|
||||
export type ProbeResultUI = z.infer<typeof ProbeResultUISchema>;
|
||||
|
||||
@@ -68,18 +68,63 @@ function badgeStatus(status?: string): BadgeProps['status'] {
|
||||
}
|
||||
}
|
||||
|
||||
function StatusDot({ status }: { status?: string }) {
|
||||
if (status === 'online') return <span className="online-dot" />;
|
||||
interface HealthProps {
|
||||
status?: string;
|
||||
xrayState?: string;
|
||||
xrayError?: string;
|
||||
}
|
||||
|
||||
// Purple: the node's panel API is reachable (status=online) but its Xray core
|
||||
// has failed or been stopped. Distinct from a normal offline/unknown node.
|
||||
const XRAY_ERROR_COLOR = '#722ED1';
|
||||
|
||||
// True when the panel is online but Xray itself reports error/stop.
|
||||
function hasXrayProblem(status?: string, xrayState?: string): boolean {
|
||||
if (status !== 'online') return false;
|
||||
const xs = (xrayState || '').toLowerCase().trim();
|
||||
return xs === 'error' || xs === 'stop';
|
||||
}
|
||||
|
||||
// Tooltip text + icon color for the status cell. A real probe error (lastError)
|
||||
// is a warning and takes precedence; otherwise an Xray-core problem shows purple.
|
||||
function statusIssue(record: Pick<NodeRecord, 'status' | 'xrayState' | 'xrayError' | 'lastError'>) {
|
||||
const tip = record.lastError || (hasXrayProblem(record.status, record.xrayState) ? record.xrayError : '') || '';
|
||||
const iconColor = !record.lastError && hasXrayProblem(record.status, record.xrayState)
|
||||
? XRAY_ERROR_COLOR
|
||||
: 'var(--ant-color-warning)';
|
||||
return { tip, iconColor };
|
||||
}
|
||||
|
||||
function StatusDot({ status, xrayState }: HealthProps) {
|
||||
if (status === 'online') {
|
||||
return hasXrayProblem(status, xrayState)
|
||||
? <span className="xray-error-dot" />
|
||||
: <span className="online-dot" />;
|
||||
}
|
||||
return <Badge status={badgeStatus(status)} />;
|
||||
}
|
||||
|
||||
function StatusLabel({ status }: { status?: string }) {
|
||||
function StatusLabel({ status, xrayState }: HealthProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<span style={status === 'online' ? { color: 'var(--ant-color-success)' } : undefined}>
|
||||
{t(`pages.nodes.statusValues.${status || 'unknown'}`)}
|
||||
</span>
|
||||
);
|
||||
if (status === 'online') {
|
||||
const xs = (xrayState || '').toLowerCase().trim();
|
||||
if (xs === 'error' || xs === 'stop') {
|
||||
const detail = xs === 'error'
|
||||
? t('pages.nodes.statusValues.xrayError')
|
||||
: t('pages.nodes.statusValues.xrayStopped');
|
||||
return (
|
||||
<span style={{ color: XRAY_ERROR_COLOR }}>
|
||||
{t('pages.nodes.statusValues.online')} ({detail})
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span style={{ color: 'var(--ant-color-success)' }}>
|
||||
{t('pages.nodes.statusValues.online')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return <span>{t(`pages.nodes.statusValues.${status || 'unknown'}`)}</span>;
|
||||
}
|
||||
|
||||
function formatPct(p?: number): string {
|
||||
@@ -271,17 +316,20 @@ export default function NodeList({
|
||||
title: t('pages.nodes.status'),
|
||||
dataIndex: 'status',
|
||||
align: 'center',
|
||||
render: (_value, record) => (
|
||||
<Space size={4}>
|
||||
<StatusDot status={record.status} />
|
||||
<StatusLabel status={record.status} />
|
||||
{record.lastError && (
|
||||
<Tooltip title={record.lastError}>
|
||||
<ExclamationCircleOutlined style={{ color: 'var(--ant-color-warning)' }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
render: (_value, record) => {
|
||||
const { tip, iconColor } = statusIssue(record);
|
||||
return (
|
||||
<Space size={4}>
|
||||
<StatusDot status={record.status} xrayState={record.xrayState} />
|
||||
<StatusLabel status={record.status} xrayState={record.xrayState} />
|
||||
{tip && (
|
||||
<Tooltip title={tip}>
|
||||
<ExclamationCircleOutlined style={{ color: iconColor }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('pages.nodes.cpu'),
|
||||
@@ -389,7 +437,7 @@ export default function NodeList({
|
||||
<div key={String(record.key)} className="node-card" style={{ paddingInlineStart: 16, opacity: 0.85 }}>
|
||||
<div className="card-head">
|
||||
<ApartmentOutlined style={{ opacity: 0.6 }} />
|
||||
<StatusDot status={record.status} />
|
||||
<StatusDot status={record.status} xrayState={record.xrayState} />
|
||||
<span className="node-name">{record.name}</span>
|
||||
<div className="card-actions">
|
||||
<Tag icon={<ApartmentOutlined />} style={{ margin: 0 }}>{t('pages.nodes.subNode')}</Tag>
|
||||
@@ -400,7 +448,7 @@ export default function NodeList({
|
||||
<div key={record.id} className="node-card">
|
||||
<div className="card-head" onClick={() => toggleExpanded(record.id)}>
|
||||
<RightOutlined className={`card-expand${expandedIds.has(record.id) ? ' is-expanded' : ''}`} />
|
||||
<StatusDot status={record.status} />
|
||||
<StatusDot status={record.status} xrayState={record.xrayState} />
|
||||
<span className="node-name">{record.name}</span>
|
||||
<div className="card-actions" onClick={(e) => e.stopPropagation()}>
|
||||
<Tooltip title={t('info')}>
|
||||
@@ -494,13 +542,16 @@ export default function NodeList({
|
||||
</div>
|
||||
<div className="stat-row">
|
||||
<span className="stat-label">{t('pages.nodes.status')}</span>
|
||||
<StatusDot status={statsNode.status} />
|
||||
<StatusLabel status={statsNode.status} />
|
||||
{statsNode.lastError && (
|
||||
<Tooltip title={statsNode.lastError}>
|
||||
<ExclamationCircleOutlined style={{ color: 'var(--ant-color-warning)' }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
<StatusDot status={statsNode.status} xrayState={statsNode.xrayState} />
|
||||
<StatusLabel status={statsNode.status} xrayState={statsNode.xrayState} />
|
||||
{(() => {
|
||||
const { tip, iconColor } = statusIssue(statsNode);
|
||||
return tip ? (
|
||||
<Tooltip title={tip}>
|
||||
<ExclamationCircleOutlined style={{ color: iconColor }} />
|
||||
</Tooltip>
|
||||
) : null;
|
||||
})()}
|
||||
</div>
|
||||
<div className="stat-row">
|
||||
<span className="stat-label">{t('pages.nodes.cpu')}</span>
|
||||
|
||||
@@ -83,12 +83,15 @@ export default function NodesPage() {
|
||||
const msg = await probe(node.id);
|
||||
if (msg?.success && msg.obj) {
|
||||
if (msg.obj.status === 'online') {
|
||||
// Even if xray is in error/stop on the node we still reached its panel API.
|
||||
messageApi.success(t('pages.nodes.connectionOk', { ms: msg.obj.latencyMs }));
|
||||
} else {
|
||||
messageApi.error(msg.obj.error || t('pages.nodes.toasts.probeFailed'));
|
||||
}
|
||||
}
|
||||
}, [probe, t, messageApi]);
|
||||
// Refresh the list so the new xrayState / xrayError (if any) appears immediately in the row.
|
||||
refetch();
|
||||
}, [probe, t, messageApi, refetch]);
|
||||
|
||||
const onToggleEnable = useCallback(async (node: NodeRecord, next: boolean) => {
|
||||
await setEnable(node.id, next);
|
||||
|
||||
@@ -23,6 +23,11 @@ export const NodeRecordSchema = z.object({
|
||||
depletedCount: z.number().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']).optional(),
|
||||
pinnedCertSha256: z.string().optional(),
|
||||
@@ -40,6 +45,9 @@ export const ProbeResultSchema = z.object({
|
||||
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({
|
||||
|
||||
@@ -41,3 +41,26 @@
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.online-dot { animation: none; }
|
||||
}
|
||||
|
||||
/* Purple indicator for nodes that are reachable via the panel API (status=online)
|
||||
but have Xray core in "error" or "stop" state. This is the new "xray failed on node"
|
||||
monitoring state. */
|
||||
.xray-error-dot {
|
||||
display: inline-block;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
margin-inline-end: 5px;
|
||||
vertical-align: middle;
|
||||
background: #722ED1;
|
||||
animation: xray-error-blink 1.1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes xray-error-blink {
|
||||
0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(114, 46, 209, 0.55); }
|
||||
50% { opacity: 0.35; box-shadow: 0 0 0 4px rgba(114, 46, 209, 0); }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.xray-error-dot { animation: none; }
|
||||
}
|
||||
|
||||
+18
-1
@@ -35,6 +35,11 @@ type HeartbeatPatch struct {
|
||||
MemPct float64
|
||||
UptimeSecs uint64
|
||||
LastError string
|
||||
// XrayState and XrayError come from the remote /panel/api/server/status when the
|
||||
// panel API is reachable. They allow distinguishing panel connectivity from
|
||||
// Xray core health on the node.
|
||||
XrayState string
|
||||
XrayError string
|
||||
}
|
||||
|
||||
type NodeService struct{}
|
||||
@@ -474,6 +479,8 @@ func (s *NodeService) UpdateHeartbeat(id int, p HeartbeatPatch) error {
|
||||
"mem_pct": p.MemPct,
|
||||
"uptime_secs": p.UptimeSecs,
|
||||
"last_error": p.LastError,
|
||||
"xray_state": p.XrayState,
|
||||
"xray_error": p.XrayError,
|
||||
}
|
||||
// Only learn the GUID; never clear a known one if an old-build node (or a
|
||||
// failed probe) reports none, so the stable identity survives blips.
|
||||
@@ -607,7 +614,9 @@ func (s *NodeService) Probe(ctx context.Context, n *model.Node) (HeartbeatPatch,
|
||||
Total uint64 `json:"total"`
|
||||
} `json:"mem"`
|
||||
Xray struct {
|
||||
Version string `json:"version"`
|
||||
Version string `json:"version"`
|
||||
State string `json:"state"`
|
||||
ErrorMsg string `json:"errorMsg"`
|
||||
} `json:"xray"`
|
||||
PanelVersion string `json:"panelVersion"`
|
||||
PanelGuid string `json:"panelGuid"`
|
||||
@@ -628,6 +637,8 @@ func (s *NodeService) Probe(ctx context.Context, n *model.Node) (HeartbeatPatch,
|
||||
patch.MemPct = float64(o.Mem.Current) * 100.0 / float64(o.Mem.Total)
|
||||
}
|
||||
patch.XrayVersion = o.Xray.Version
|
||||
patch.XrayState = o.Xray.State
|
||||
patch.XrayError = o.Xray.ErrorMsg
|
||||
patch.PanelVersion = o.PanelVersion
|
||||
patch.Guid = o.PanelGuid
|
||||
patch.UptimeSecs = o.Uptime
|
||||
@@ -643,6 +654,10 @@ type ProbeResultUI struct {
|
||||
MemPct float64 `json:"memPct" example:"45.2"`
|
||||
UptimeSecs uint64 `json:"uptimeSecs" example:"86400"`
|
||||
Error string `json:"error"`
|
||||
// XrayState/XrayError are populated on successful probes even when the node's
|
||||
// Xray core is not healthy. The UI uses them for a distinct "panel ok, xray failed" indicator.
|
||||
XrayState string `json:"xrayState"`
|
||||
XrayError string `json:"xrayError"`
|
||||
}
|
||||
|
||||
func (p HeartbeatPatch) ToUI(ok bool) ProbeResultUI {
|
||||
@@ -654,6 +669,8 @@ func (p HeartbeatPatch) ToUI(ok bool) ProbeResultUI {
|
||||
MemPct: p.MemPct,
|
||||
UptimeSecs: p.UptimeSecs,
|
||||
Error: FriendlyProbeError(p.LastError),
|
||||
XrayState: p.XrayState,
|
||||
XrayError: p.XrayError,
|
||||
}
|
||||
if ok {
|
||||
r.Status = "online"
|
||||
|
||||
@@ -40,6 +40,8 @@ func (s *NodeService) LocalDescendants() ([]model.NodeSummary, error) {
|
||||
LatencyMs: n.LatencyMs,
|
||||
PanelVersion: n.PanelVersion,
|
||||
XrayVersion: n.XrayVersion,
|
||||
XrayState: n.XrayState,
|
||||
XrayError: n.XrayError,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
@@ -140,6 +142,8 @@ func (s *NodeService) GetNodeTree() ([]*model.Node, error) {
|
||||
LatencyMs: sum.LatencyMs,
|
||||
PanelVersion: sum.PanelVersion,
|
||||
XrayVersion: sum.XrayVersion,
|
||||
XrayState: sum.XrayState,
|
||||
XrayError: sum.XrayError,
|
||||
Transitive: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -896,7 +896,9 @@
|
||||
"statusValues": {
|
||||
"online": "متصل",
|
||||
"offline": "غير متصل",
|
||||
"unknown": "غير معروف"
|
||||
"unknown": "غير معروف",
|
||||
"xrayError": "خطأ Xray",
|
||||
"xrayStopped": "متوقف"
|
||||
},
|
||||
"toasts": {
|
||||
"list": "فشل تحميل النودز",
|
||||
|
||||
@@ -897,7 +897,9 @@
|
||||
"statusValues": {
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"unknown": "Unknown"
|
||||
"unknown": "Unknown",
|
||||
"xrayError": "Xray Error",
|
||||
"xrayStopped": "Stopped"
|
||||
},
|
||||
"toasts": {
|
||||
"list": "Failed to load nodes",
|
||||
|
||||
@@ -896,7 +896,9 @@
|
||||
"statusValues": {
|
||||
"online": "En línea",
|
||||
"offline": "Sin conexión",
|
||||
"unknown": "Desconocido"
|
||||
"unknown": "Desconocido",
|
||||
"xrayError": "Error de Xray",
|
||||
"xrayStopped": "Detenido"
|
||||
},
|
||||
"toasts": {
|
||||
"list": "Error al cargar los nodos",
|
||||
|
||||
@@ -896,7 +896,9 @@
|
||||
"statusValues": {
|
||||
"online": "آنلاین",
|
||||
"offline": "آفلاین",
|
||||
"unknown": "نامشخص"
|
||||
"unknown": "نامشخص",
|
||||
"xrayError": "خطای Xray",
|
||||
"xrayStopped": "متوقف"
|
||||
},
|
||||
"toasts": {
|
||||
"list": "بارگذاری نودها ناموفق",
|
||||
|
||||
@@ -896,7 +896,9 @@
|
||||
"statusValues": {
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"unknown": "Tidak diketahui"
|
||||
"unknown": "Tidak diketahui",
|
||||
"xrayError": "Kesalahan Xray",
|
||||
"xrayStopped": "Berhenti"
|
||||
},
|
||||
"toasts": {
|
||||
"list": "Gagal memuat node",
|
||||
|
||||
@@ -896,7 +896,9 @@
|
||||
"statusValues": {
|
||||
"online": "オンライン",
|
||||
"offline": "オフライン",
|
||||
"unknown": "不明"
|
||||
"unknown": "不明",
|
||||
"xrayError": "Xray エラー",
|
||||
"xrayStopped": "停止"
|
||||
},
|
||||
"toasts": {
|
||||
"list": "ノードの読み込みに失敗しました",
|
||||
|
||||
@@ -896,7 +896,9 @@
|
||||
"statusValues": {
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"unknown": "Desconhecido"
|
||||
"unknown": "Desconhecido",
|
||||
"xrayError": "Erro do Xray",
|
||||
"xrayStopped": "Parado"
|
||||
},
|
||||
"toasts": {
|
||||
"list": "Falha ao carregar os nós",
|
||||
|
||||
@@ -896,7 +896,9 @@
|
||||
"statusValues": {
|
||||
"online": "В сети",
|
||||
"offline": "Не в сети",
|
||||
"unknown": "Неизвестно"
|
||||
"unknown": "Неизвестно",
|
||||
"xrayError": "Ошибка Xray",
|
||||
"xrayStopped": "Остановлен"
|
||||
},
|
||||
"toasts": {
|
||||
"list": "Не удалось загрузить узлы",
|
||||
|
||||
@@ -896,7 +896,9 @@
|
||||
"statusValues": {
|
||||
"online": "Çevrimiçi",
|
||||
"offline": "Çevrimdışı",
|
||||
"unknown": "Bilinmiyor"
|
||||
"unknown": "Bilinmiyor",
|
||||
"xrayError": "Xray Hatası",
|
||||
"xrayStopped": "Durduruldu"
|
||||
},
|
||||
"toasts": {
|
||||
"list": "Düğümler yüklenemedi",
|
||||
|
||||
@@ -896,7 +896,9 @@
|
||||
"statusValues": {
|
||||
"online": "У мережі",
|
||||
"offline": "Не в мережі",
|
||||
"unknown": "Невідомо"
|
||||
"unknown": "Невідомо",
|
||||
"xrayError": "Помилка Xray",
|
||||
"xrayStopped": "Зупинено"
|
||||
},
|
||||
"toasts": {
|
||||
"list": "Не вдалося завантажити вузли",
|
||||
|
||||
@@ -896,7 +896,9 @@
|
||||
"statusValues": {
|
||||
"online": "Trực tuyến",
|
||||
"offline": "Ngoại tuyến",
|
||||
"unknown": "Không xác định"
|
||||
"unknown": "Không xác định",
|
||||
"xrayError": "Lỗi Xray",
|
||||
"xrayStopped": "Đã dừng"
|
||||
},
|
||||
"toasts": {
|
||||
"list": "Không tải được danh sách nút",
|
||||
|
||||
@@ -896,7 +896,9 @@
|
||||
"statusValues": {
|
||||
"online": "在线",
|
||||
"offline": "离线",
|
||||
"unknown": "未知"
|
||||
"unknown": "未知",
|
||||
"xrayError": "Xray 错误",
|
||||
"xrayStopped": "已停止"
|
||||
},
|
||||
"toasts": {
|
||||
"list": "加载节点失败",
|
||||
|
||||
@@ -896,7 +896,9 @@
|
||||
"statusValues": {
|
||||
"online": "上線",
|
||||
"offline": "離線",
|
||||
"unknown": "未知"
|
||||
"unknown": "未知",
|
||||
"xrayError": "Xray 錯誤",
|
||||
"xrayStopped": "已停止"
|
||||
},
|
||||
"toasts": {
|
||||
"list": "載入節點失敗",
|
||||
|
||||
Reference in New Issue
Block a user