From 1c74b995c3a340c77764fccc306999608cc2a310 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rouzbeh=E2=80=A0?= <78313022+rqzbeh@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:24:00 +0200 Subject: [PATCH] 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 Co-authored-by: Sanaei --- database/model/model.go | 18 +++-- frontend/public/openapi.json | 22 +++++ frontend/src/generated/examples.ts | 4 + frontend/src/generated/schemas.ts | 18 +++++ frontend/src/generated/types.ts | 4 + frontend/src/generated/zod.ts | 4 + frontend/src/pages/nodes/NodeList.tsx | 107 ++++++++++++++++++------- frontend/src/pages/nodes/NodesPage.tsx | 5 +- frontend/src/schemas/node.ts | 8 ++ frontend/src/styles/utils.css | 23 ++++++ web/service/node.go | 19 ++++- web/service/node_tree.go | 4 + web/translation/ar-EG.json | 4 +- web/translation/en-US.json | 4 +- web/translation/es-ES.json | 4 +- web/translation/fa-IR.json | 4 +- web/translation/id-ID.json | 4 +- web/translation/ja-JP.json | 4 +- web/translation/pt-BR.json | 4 +- web/translation/ru-RU.json | 4 +- web/translation/tr-TR.json | 4 +- web/translation/uk-UA.json | 4 +- web/translation/vi-VN.json | 4 +- web/translation/zh-CN.json | 4 +- web/translation/zh-TW.json | 4 +- 25 files changed, 239 insertions(+), 49 deletions(-) diff --git a/database/model/model.go b/database/model/model.go index 9da063f7b..ff4099597 100644 --- a/database/model/model.go +++ b/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 { diff --git a/frontend/public/openapi.json b/frontend/public/openapi.json index 56385135b..d2cd03500 100644 --- a/frontend/public/openapi.json +++ b/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" } } diff --git a/frontend/src/generated/examples.ts b/frontend/src/generated/examples.ts index b0b791cc1..17bed5c68 100644 --- a/frontend/src/generated/examples.ts +++ b/frontend/src/generated/examples.ts @@ -364,6 +364,8 @@ export const EXAMPLES: Record = { "transitive": false, "updatedAt": 1700000000, "uptimeSecs": 86400, + "xrayError": "", + "xrayState": "", "xrayVersion": "25.10.31" }, "OutboundTraffics": { @@ -381,6 +383,8 @@ export const EXAMPLES: Record = { "panelVersion": "v3.x.x", "status": "online", "uptimeSecs": 86400, + "xrayError": "", + "xrayState": "", "xrayVersion": "25.10.31" }, "Setting": { diff --git a/frontend/src/generated/schemas.ts b/frontend/src/generated/schemas.ts index 00471dc00..77207c750 100644 --- a/frontend/src/generated/schemas.ts +++ b/frontend/src/generated/schemas.ts @@ -1630,6 +1630,13 @@ export const SCHEMAS: Record = { "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 = { "tlsVerifyMode", "updatedAt", "uptimeSecs", + "xrayError", + "xrayState", "xrayVersion" ], "type": "object" @@ -1726,6 +1735,13 @@ export const SCHEMAS: Record = { "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 = { "panelVersion", "status", "uptimeSecs", + "xrayError", + "xrayState", "xrayVersion" ], "type": "object" diff --git a/frontend/src/generated/types.ts b/frontend/src/generated/types.ts index cb2d64f72..a4adf2366 100644 --- a/frontend/src/generated/types.ts +++ b/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; } diff --git a/frontend/src/generated/zod.ts b/frontend/src/generated/zod.ts index 14c1f909a..296207768 100644 --- a/frontend/src/generated/zod.ts +++ b/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; @@ -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; diff --git a/frontend/src/pages/nodes/NodeList.tsx b/frontend/src/pages/nodes/NodeList.tsx index 8c1694026..403cf7cd4 100644 --- a/frontend/src/pages/nodes/NodeList.tsx +++ b/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 ; +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) { + 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) + ? + : ; + } return ; } -function StatusLabel({ status }: { status?: string }) { +function StatusLabel({ status, xrayState }: HealthProps) { const { t } = useTranslation(); - return ( - - {t(`pages.nodes.statusValues.${status || 'unknown'}`)} - - ); + 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 ( + + {t('pages.nodes.statusValues.online')} ({detail}) + + ); + } + return ( + + {t('pages.nodes.statusValues.online')} + + ); + } + return {t(`pages.nodes.statusValues.${status || 'unknown'}`)}; } 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) => ( - - - - {record.lastError && ( - - - - )} - - ), + render: (_value, record) => { + const { tip, iconColor } = statusIssue(record); + return ( + + + + {tip && ( + + + + )} + + ); + }, }, { title: t('pages.nodes.cpu'), @@ -389,7 +437,7 @@ export default function NodeList({
- + {record.name}
} style={{ margin: 0 }}>{t('pages.nodes.subNode')} @@ -400,7 +448,7 @@ export default function NodeList({
toggleExpanded(record.id)}> - + {record.name}
e.stopPropagation()}> @@ -494,13 +542,16 @@ export default function NodeList({
{t('pages.nodes.status')} - - - {statsNode.lastError && ( - - - - )} + + + {(() => { + const { tip, iconColor } = statusIssue(statsNode); + return tip ? ( + + + + ) : null; + })()}
{t('pages.nodes.cpu')} diff --git a/frontend/src/pages/nodes/NodesPage.tsx b/frontend/src/pages/nodes/NodesPage.tsx index d5b23596d..1ee36f657 100644 --- a/frontend/src/pages/nodes/NodesPage.tsx +++ b/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); diff --git a/frontend/src/schemas/node.ts b/frontend/src/schemas/node.ts index 9e16d507f..2684a9220 100644 --- a/frontend/src/schemas/node.ts +++ b/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({ diff --git a/frontend/src/styles/utils.css b/frontend/src/styles/utils.css index 879a26877..7bdc5e360 100644 --- a/frontend/src/styles/utils.css +++ b/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; } +} diff --git a/web/service/node.go b/web/service/node.go index 18e52ca08..14e4ccfcc 100644 --- a/web/service/node.go +++ b/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" diff --git a/web/service/node_tree.go b/web/service/node_tree.go index 938cc8934..b20e2e877 100644 --- a/web/service/node_tree.go +++ b/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, }) } diff --git a/web/translation/ar-EG.json b/web/translation/ar-EG.json index f78766dc0..a7f407cbe 100644 --- a/web/translation/ar-EG.json +++ b/web/translation/ar-EG.json @@ -896,7 +896,9 @@ "statusValues": { "online": "متصل", "offline": "غير متصل", - "unknown": "غير معروف" + "unknown": "غير معروف", + "xrayError": "خطأ Xray", + "xrayStopped": "متوقف" }, "toasts": { "list": "فشل تحميل النودز", diff --git a/web/translation/en-US.json b/web/translation/en-US.json index 75bf5e0d9..663322b79 100644 --- a/web/translation/en-US.json +++ b/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", diff --git a/web/translation/es-ES.json b/web/translation/es-ES.json index f9ccbcd7d..e3d1ff646 100644 --- a/web/translation/es-ES.json +++ b/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", diff --git a/web/translation/fa-IR.json b/web/translation/fa-IR.json index 6af6a2fdf..179a6df0b 100644 --- a/web/translation/fa-IR.json +++ b/web/translation/fa-IR.json @@ -896,7 +896,9 @@ "statusValues": { "online": "آنلاین", "offline": "آفلاین", - "unknown": "نامشخص" + "unknown": "نامشخص", + "xrayError": "خطای Xray", + "xrayStopped": "متوقف" }, "toasts": { "list": "بارگذاری نودها ناموفق", diff --git a/web/translation/id-ID.json b/web/translation/id-ID.json index a556d989d..6e68198c7 100644 --- a/web/translation/id-ID.json +++ b/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", diff --git a/web/translation/ja-JP.json b/web/translation/ja-JP.json index 1cecf52c3..8a9e3479c 100644 --- a/web/translation/ja-JP.json +++ b/web/translation/ja-JP.json @@ -896,7 +896,9 @@ "statusValues": { "online": "オンライン", "offline": "オフライン", - "unknown": "不明" + "unknown": "不明", + "xrayError": "Xray エラー", + "xrayStopped": "停止" }, "toasts": { "list": "ノードの読み込みに失敗しました", diff --git a/web/translation/pt-BR.json b/web/translation/pt-BR.json index 65ffd1c75..9e01ffea2 100644 --- a/web/translation/pt-BR.json +++ b/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", diff --git a/web/translation/ru-RU.json b/web/translation/ru-RU.json index f8f9100a9..0bcb4b03f 100644 --- a/web/translation/ru-RU.json +++ b/web/translation/ru-RU.json @@ -896,7 +896,9 @@ "statusValues": { "online": "В сети", "offline": "Не в сети", - "unknown": "Неизвестно" + "unknown": "Неизвестно", + "xrayError": "Ошибка Xray", + "xrayStopped": "Остановлен" }, "toasts": { "list": "Не удалось загрузить узлы", diff --git a/web/translation/tr-TR.json b/web/translation/tr-TR.json index 3a4c0655c..99052f515 100644 --- a/web/translation/tr-TR.json +++ b/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", diff --git a/web/translation/uk-UA.json b/web/translation/uk-UA.json index 59f3c48af..f24752811 100644 --- a/web/translation/uk-UA.json +++ b/web/translation/uk-UA.json @@ -896,7 +896,9 @@ "statusValues": { "online": "У мережі", "offline": "Не в мережі", - "unknown": "Невідомо" + "unknown": "Невідомо", + "xrayError": "Помилка Xray", + "xrayStopped": "Зупинено" }, "toasts": { "list": "Не вдалося завантажити вузли", diff --git a/web/translation/vi-VN.json b/web/translation/vi-VN.json index 7da171254..bd31b2aaa 100644 --- a/web/translation/vi-VN.json +++ b/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", diff --git a/web/translation/zh-CN.json b/web/translation/zh-CN.json index d401fa285..2abb012b8 100644 --- a/web/translation/zh-CN.json +++ b/web/translation/zh-CN.json @@ -896,7 +896,9 @@ "statusValues": { "online": "在线", "offline": "离线", - "unknown": "未知" + "unknown": "未知", + "xrayError": "Xray 错误", + "xrayStopped": "已停止" }, "toasts": { "list": "加载节点失败", diff --git a/web/translation/zh-TW.json b/web/translation/zh-TW.json index 8a99916ea..8c0f78113 100644 --- a/web/translation/zh-TW.json +++ b/web/translation/zh-TW.json @@ -896,7 +896,9 @@ "statusValues": { "online": "上線", "offline": "離線", - "unknown": "未知" + "unknown": "未知", + "xrayError": "Xray 錯誤", + "xrayStopped": "已停止" }, "toasts": { "list": "載入節點失敗",