Преглед на файлове

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 <[email protected]>
Co-authored-by: Sanaei <[email protected]>
Rouzbeh† преди 7 часа
родител
ревизия
1c74b995c3

+ 12 - 6
database/model/model.go

@@ -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 {

+ 22 - 0
frontend/public/openapi.json

@@ -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"
                   }
                 }

+ 4 - 0
frontend/src/generated/examples.ts

@@ -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": {

+ 18 - 0
frontend/src/generated/schemas.ts

@@ -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"

+ 4 - 0
frontend/src/generated/types.ts

@@ -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;
 }
 

+ 4 - 0
frontend/src/generated/zod.ts

@@ -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>;

+ 79 - 28
frontend/src/pages/nodes/NodeList.tsx

@@ -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>

+ 4 - 1
frontend/src/pages/nodes/NodesPage.tsx

@@ -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);

+ 8 - 0
frontend/src/schemas/node.ts

@@ -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({

+ 23 - 0
frontend/src/styles/utils.css

@@ -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
web/service/node.go

@@ -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"

+ 4 - 0
web/service/node_tree.go

@@ -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,
 		})
 	}

+ 3 - 1
web/translation/ar-EG.json

@@ -896,7 +896,9 @@
       "statusValues": {
         "online": "متصل",
         "offline": "غير متصل",
-        "unknown": "غير معروف"
+        "unknown": "غير معروف",
+        "xrayError": "خطأ Xray",
+        "xrayStopped": "متوقف"
       },
       "toasts": {
         "list": "فشل تحميل النودز",

+ 3 - 1
web/translation/en-US.json

@@ -897,7 +897,9 @@
       "statusValues": {
         "online": "Online",
         "offline": "Offline",
-        "unknown": "Unknown"
+        "unknown": "Unknown",
+        "xrayError": "Xray Error",
+        "xrayStopped": "Stopped"
       },
       "toasts": {
         "list": "Failed to load nodes",

+ 3 - 1
web/translation/es-ES.json

@@ -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",

+ 3 - 1
web/translation/fa-IR.json

@@ -896,7 +896,9 @@
       "statusValues": {
         "online": "آنلاین",
         "offline": "آفلاین",
-        "unknown": "نامشخص"
+        "unknown": "نامشخص",
+        "xrayError": "خطای Xray",
+        "xrayStopped": "متوقف"
       },
       "toasts": {
         "list": "بارگذاری نودها ناموفق",

+ 3 - 1
web/translation/id-ID.json

@@ -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",

+ 3 - 1
web/translation/ja-JP.json

@@ -896,7 +896,9 @@
       "statusValues": {
         "online": "オンライン",
         "offline": "オフライン",
-        "unknown": "不明"
+        "unknown": "不明",
+        "xrayError": "Xray エラー",
+        "xrayStopped": "停止"
       },
       "toasts": {
         "list": "ノードの読み込みに失敗しました",

+ 3 - 1
web/translation/pt-BR.json

@@ -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",

+ 3 - 1
web/translation/ru-RU.json

@@ -896,7 +896,9 @@
       "statusValues": {
         "online": "В сети",
         "offline": "Не в сети",
-        "unknown": "Неизвестно"
+        "unknown": "Неизвестно",
+        "xrayError": "Ошибка Xray",
+        "xrayStopped": "Остановлен"
       },
       "toasts": {
         "list": "Не удалось загрузить узлы",

+ 3 - 1
web/translation/tr-TR.json

@@ -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",

+ 3 - 1
web/translation/uk-UA.json

@@ -896,7 +896,9 @@
       "statusValues": {
         "online": "У мережі",
         "offline": "Не в мережі",
-        "unknown": "Невідомо"
+        "unknown": "Невідомо",
+        "xrayError": "Помилка Xray",
+        "xrayStopped": "Зупинено"
       },
       "toasts": {
         "list": "Не вдалося завантажити вузли",

+ 3 - 1
web/translation/vi-VN.json

@@ -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",

+ 3 - 1
web/translation/zh-CN.json

@@ -896,7 +896,9 @@
       "statusValues": {
         "online": "在线",
         "offline": "离线",
-        "unknown": "未知"
+        "unknown": "未知",
+        "xrayError": "Xray 错误",
+        "xrayStopped": "已停止"
       },
       "toasts": {
         "list": "加载节点失败",

+ 3 - 1
web/translation/zh-TW.json

@@ -896,7 +896,9 @@
       "statusValues": {
         "online": "上線",
         "offline": "離線",
-        "unknown": "未知"
+        "unknown": "未知",
+        "xrayError": "Xray 錯誤",
+        "xrayStopped": "已停止"
       },
       "toasts": {
         "list": "載入節點失敗",