diff --git a/frontend/src/pages/index/IndexPage.vue b/frontend/src/pages/index/IndexPage.vue index 292e4156..0551c2b6 100644 --- a/frontend/src/pages/index/IndexPage.vue +++ b/frontend/src/pages/index/IndexPage.vue @@ -14,6 +14,10 @@ import { SwapOutlined, EyeOutlined, EyeInvisibleOutlined, + ThunderboltOutlined, + DesktopOutlined, + DatabaseOutlined, + ForkOutlined, } from '@ant-design/icons-vue'; const { t } = useI18n(); @@ -31,6 +35,7 @@ import PanelUpdateModal from './PanelUpdateModal.vue'; import LogModal from './LogModal.vue'; import BackupModal from './BackupModal.vue'; import SystemHistoryModal from './SystemHistoryModal.vue'; +import XrayMetricsModal from './XrayMetricsModal.vue'; import XrayLogModal from './XrayLogModal.vue'; import VersionModal from './VersionModal.vue'; @@ -71,6 +76,7 @@ const logsOpen = ref(false); const backupOpen = ref(false); const panelUpdateOpen = ref(false); const sysHistoryOpen = ref(false); +const xrayMetricsOpen = ref(false); const xrayLogsOpen = ref(false); const versionOpen = ref(false); const configTextOpen = ref(false); @@ -98,6 +104,18 @@ function openSystemHistory() { sysHistoryOpen.value = true; } function openXrayLogs() { xrayLogsOpen.value = true; } function openVersionSwitch() { versionOpen.value = true; } +function openPanelVersion() { + if (panelUpdateInfo.value.updateAvailable) { + panelUpdateOpen.value = true; + } else { + window.open('https://github.com/MHSanaei/3x-ui/releases', '_blank', 'noopener,noreferrer'); + } +} + +function openTelegram() { + window.open('https://t.me/XrayUI', '_blank', 'noopener,noreferrer'); +} + // Legacy "Config" action — fetch the rendered xray config and show // it as JSON in the shared TextModal (same UX as main). async function openConfig() { @@ -155,62 +173,83 @@ async function openConfig() { - - - - - {{ panelUpdateInfo.latestVersion }} - {{ t('pages.index.updatePanel') }} - - + + + + + + @XrayUI + + + + + {{ panelUpdateInfo.updateAvailable + ? `${t('update')} ${panelUpdateInfo.latestVersion}` + : `v${displayVersion}` }} + + + + + + + + + + + + {{ t('pages.index.systemHistoryTitle') }} + + + + {{ t('pages.index.xrayMetricsTitle') }} + - - - v{{ displayVersion }} - - - @XrayUI - - - {{ t('pages.index.documentation') }} - - - - Xray: {{ TimeFormatter.formatSecond(status.appStats.uptime) }} - - OS: {{ TimeFormatter.formatSecond(status.uptime) }} - - - - - - - - - {{ t('pages.index.systemHistoryTitle') }} - - - - - {{ status.loads[0] }} | {{ status.loads[1] }} | {{ status.loads[2] }} - - + + + + + + + + + + + + + + + + - - {{ t('pages.index.memory') }}: {{ SizeFormatter.sizeFormat(status.appStats.mem) }} - - - {{ t('pages.index.threads') }}: {{ status.appStats.threads }} - + + + + + + + + + + + + + + + + @@ -318,6 +357,7 @@ async function openConfig() { + +import { computed, ref, watch } from 'vue'; +import { useI18n } from 'vue-i18n'; +import { HttpUtil, SizeFormatter } from '@/utils'; +import Sparkline from '@/components/Sparkline.vue'; +import { useMediaQuery } from '@/composables/useMediaQuery.js'; + +const { t } = useI18n(); +const { isMobile } = useMediaQuery(); +const modalWidth = computed(() => (isMobile.value ? '95vw' : '900px')); + +const props = defineProps({ + open: { type: Boolean, default: false }, +}); +const emit = defineEmits(['update:open']); + +const OBS_KEY = 'xrObs'; + +const metrics = [ + { key: 'xrAlloc', tab: 'Heap', unit: 'B', stroke: '#7c4dff' }, + { key: 'xrSys', tab: 'Sys', unit: 'B', stroke: '#1890ff' }, + { key: 'xrHeapObjects', tab: 'Objects', unit: '', stroke: '#13c2c2' }, + { key: 'xrNumGC', tab: 'GC Count', unit: '', stroke: '#fa8c16' }, + { key: 'xrPauseNs', tab: 'GC Pause', unit: 'ns', stroke: '#f5222d' }, + { key: OBS_KEY, tab: 'Observatory', unit: 'ms', stroke: '#52c41a' }, +]; + +const activeKey = ref('xrAlloc'); +const bucket = ref(2); +const points = ref([]); +const labels = ref([]); +const state = ref({ enabled: false, listen: '', reason: '' }); +const obsTags = ref([]); +const obsActiveTag = ref(''); +let obsTimer = null; + +const activeMetric = computed(() => metrics.find((m) => m.key === activeKey.value)); +const isObservatory = computed(() => activeKey.value === OBS_KEY); +const strokeColor = computed(() => activeMetric.value?.stroke || '#008771'); +const activeObsTag = computed(() => obsTags.value.find((tg) => tg.tag === obsActiveTag.value) || null); + +function unitFormatter(unit) { + if (unit === 'B') { + return (v) => SizeFormatter.sizeFormat(Math.max(0, Number(v) || 0)); + } + if (unit === 'ns') { + return (v) => { + const n = Math.max(0, Number(v) || 0); + if (n >= 1e6) return `${(n / 1e6).toFixed(2)} ms`; + if (n >= 1e3) return `${(n / 1e3).toFixed(1)} µs`; + return `${n.toFixed(0)} ns`; + }; + } + if (unit === 'ms') { + return (v) => `${Math.round(Number(v) || 0)} ms`; + } + return (v) => { + const n = Number(v) || 0; + return Math.round(n).toLocaleString(); + }; +} + +const yFormatter = computed(() => unitFormatter(activeMetric.value?.unit ?? '')); + +function fmtTimestamp(unixSec) { + if (!unixSec) return '—'; + const d = new Date(unixSec * 1000); + const hh = String(d.getHours()).padStart(2, '0'); + const mm = String(d.getMinutes()).padStart(2, '0'); + const ss = String(d.getSeconds()).padStart(2, '0'); + return `${d.toLocaleDateString()} ${hh}:${mm}:${ss}`; +} + +async function fetchState() { + try { + const msg = await HttpUtil.get('/panel/api/server/xrayMetricsState'); + if (msg?.success && msg.obj) state.value = msg.obj; + } catch (e) { + console.error('Failed to fetch xray metrics state', e); + } +} + +async function fetchObservatory() { + try { + const msg = await HttpUtil.get('/panel/api/server/xrayObservatory'); + if (msg?.success && Array.isArray(msg.obj)) { + obsTags.value = msg.obj; + if (!obsTags.value.find((tg) => tg.tag === obsActiveTag.value)) { + obsActiveTag.value = obsTags.value[0]?.tag || ''; + } + } else { + obsTags.value = []; + } + } catch (e) { + console.error('Failed to fetch observatory snapshot', e); + obsTags.value = []; + } +} + +async function fetchMetricBucket() { + const m = activeMetric.value; + if (!m) return; + try { + const url = `/panel/api/server/xrayMetricsHistory/${m.key}/${bucket.value}`; + const msg = await HttpUtil.get(url); + applyHistory(msg); + } catch (e) { + console.error('Failed to fetch xray metrics bucket', e); + labels.value = []; + points.value = []; + } +} + +async function fetchObsBucket() { + const tag = obsActiveTag.value; + if (!tag) { + labels.value = []; + points.value = []; + return; + } + try { + const url = `/panel/api/server/xrayObservatoryHistory/${encodeURIComponent(tag)}/${bucket.value}`; + const msg = await HttpUtil.get(url); + applyHistory(msg); + } catch (e) { + console.error('Failed to fetch observatory bucket', e); + labels.value = []; + points.value = []; + } +} + +function applyHistory(msg) { + if (msg?.success && Array.isArray(msg.obj)) { + const vals = []; + const labs = []; + for (const p of msg.obj) { + const d = new Date(p.t * 1000); + const hh = String(d.getHours()).padStart(2, '0'); + const mm = String(d.getMinutes()).padStart(2, '0'); + const ss = String(d.getSeconds()).padStart(2, '0'); + labs.push(bucket.value >= 60 ? `${hh}:${mm}` : `${hh}:${mm}:${ss}`); + vals.push(Number(p.v) || 0); + } + labels.value = labs; + points.value = vals; + } else { + labels.value = []; + points.value = []; + } +} + +function refreshActive() { + if (isObservatory.value) { + fetchObsBucket(); + } else { + fetchMetricBucket(); + } +} + +function startObsPolling() { + stopObsPolling(); + obsTimer = window.setInterval(async () => { + if (!props.open || !isObservatory.value) return; + await fetchObservatory(); + fetchObsBucket(); + }, 2000); +} + +function stopObsPolling() { + if (obsTimer != null) { + window.clearInterval(obsTimer); + obsTimer = null; + } +} + +function close() { + emit('update:open', false); +} + +watch(() => props.open, (next) => { + if (next) { + activeKey.value = 'xrAlloc'; + fetchState(); + fetchMetricBucket(); + } else { + stopObsPolling(); + } +}); + +watch(activeKey, async (key) => { + if (!props.open) return; + if (key === OBS_KEY) { + await fetchObservatory(); + fetchObsBucket(); + startObsPolling(); + } else { + stopObsPolling(); + fetchMetricBucket(); + } +}); + +watch(bucket, () => { + if (props.open) refreshActive(); +}); + +watch(obsActiveTag, () => { + if (props.open && isObservatory.value) fetchObsBucket(); +}); + + + + + + {{ t('pages.index.xrayMetricsTitle') }} + + 2m + 30m + 1h + 2h + 3h + 5h + + + + + + + + + + + + + + + + + {{ tg.tag }} + + + + + + {{ activeObsTag.alive ? t('pages.index.xrayObservatoryAlive') : t('pages.index.xrayObservatoryDead') }} + + {{ activeObsTag.delay }} ms + + {{ t('pages.index.xrayObservatoryLastSeen') }}: {{ fmtTimestamp(activeObsTag.lastSeenTime) }} + + + {{ t('pages.index.xrayObservatoryLastTry') }}: {{ fmtTimestamp(activeObsTag.lastTryTime) }} + + + + + + + + Timeframe: {{ bucket }} sec per point (total {{ points.length }} points) + · {{ state.listen }} + + + + + + + diff --git a/web/controller/server.go b/web/controller/server.go index 441a0d88..4d5aa356 100644 --- a/web/controller/server.go +++ b/web/controller/server.go @@ -23,9 +23,10 @@ var filenameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-.]+$`) type ServerController struct { BaseController - serverService service.ServerService - settingService service.SettingService - panelService service.PanelService + serverService service.ServerService + settingService service.SettingService + panelService service.PanelService + xrayMetricsService service.XrayMetricsService lastStatus *service.Status @@ -47,6 +48,10 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) { g.GET("/status", a.status) g.GET("/cpuHistory/:bucket", a.getCpuHistoryBucket) g.GET("/history/:metric/:bucket", a.getMetricHistoryBucket) + g.GET("/xrayMetricsState", a.getXrayMetricsState) + g.GET("/xrayMetricsHistory/:metric/:bucket", a.getXrayMetricsHistoryBucket) + g.GET("/xrayObservatory", a.getXrayObservatory) + g.GET("/xrayObservatoryHistory/:tag/:bucket", a.getXrayObservatoryHistoryBucket) g.GET("/getXrayVersion", a.getXrayVersion) g.GET("/getPanelUpdateInfo", a.getPanelUpdateInfo) g.GET("/getConfigJson", a.getConfigJson) @@ -75,7 +80,9 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) { func (a *ServerController) refreshStatus() { a.lastStatus = a.serverService.GetStatus(a.lastStatus) if a.lastStatus != nil { - a.serverService.AppendStatusSample(time.Now(), a.lastStatus) + now := time.Now() + a.serverService.AppendStatusSample(now, a.lastStatus) + a.xrayMetricsService.Sample(now) // Broadcast status update via WebSocket websocket.BroadcastStatus(a.lastStatus) } @@ -143,6 +150,42 @@ func (a *ServerController) getMetricHistoryBucket(c *gin.Context) { jsonObj(c, a.serverService.AggregateSystemMetric(metric, bucket, 60), nil) } +func (a *ServerController) getXrayMetricsState(c *gin.Context) { + jsonObj(c, a.xrayMetricsService.State(), nil) +} + +func (a *ServerController) getXrayMetricsHistoryBucket(c *gin.Context) { + metric := c.Param("metric") + if !slices.Contains(service.XrayMetricKeys, metric) { + jsonMsg(c, "invalid metric", fmt.Errorf("unknown metric")) + return + } + bucket, err := strconv.Atoi(c.Param("bucket")) + if err != nil || bucket <= 0 || !allowedHistoryBuckets[bucket] { + jsonMsg(c, "invalid bucket", fmt.Errorf("unsupported bucket")) + return + } + jsonObj(c, a.xrayMetricsService.AggregateMetric(metric, bucket, 60), nil) +} + +func (a *ServerController) getXrayObservatory(c *gin.Context) { + jsonObj(c, a.xrayMetricsService.ObservatorySnapshot(), nil) +} + +func (a *ServerController) getXrayObservatoryHistoryBucket(c *gin.Context) { + tag := c.Param("tag") + if !a.xrayMetricsService.HasObservatoryTag(tag) { + jsonMsg(c, "invalid tag", fmt.Errorf("unknown observatory tag")) + return + } + bucket, err := strconv.Atoi(c.Param("bucket")) + if err != nil || bucket <= 0 || !allowedHistoryBuckets[bucket] { + jsonMsg(c, "invalid bucket", fmt.Errorf("unsupported bucket")) + return + } + jsonObj(c, a.xrayMetricsService.AggregateObservatory(tag, bucket, 60), nil) +} + func (a *ServerController) getXrayVersion(c *gin.Context) { const cacheTTLSeconds = 15 * 60 diff --git a/web/service/metric_history.go b/web/service/metric_history.go index f180d3b9..5905b678 100644 --- a/web/service/metric_history.go +++ b/web/service/metric_history.go @@ -130,6 +130,7 @@ func (h *metricHistory) aggregate(metric string, bucketSeconds int, maxPoints in var ( systemMetrics = newMetricHistory() nodeMetrics = newMetricHistory() + xrayMetrics = newMetricHistory() ) // SystemMetricKeys lists the metric names ServerService writes on every @@ -141,3 +142,11 @@ var SystemMetricKeys = []string{ // NodeMetricKeys lists the per-node metric names NodeHeartbeatJob writes. var NodeMetricKeys = []string{"cpu", "mem"} + +// XrayMetricKeys lists series sourced from xray's /debug/vars expvar +// endpoint. Populated by XrayMetricsService.Sample on the same 2s cadence +// as the system metrics, but only when the xray config has a `metrics` +// block configured. +var XrayMetricKeys = []string{ + "xrAlloc", "xrSys", "xrHeapObjects", "xrNumGC", "xrPauseNs", +} diff --git a/web/service/xray_metrics.go b/web/service/xray_metrics.go new file mode 100644 index 00000000..9eb08039 --- /dev/null +++ b/web/service/xray_metrics.go @@ -0,0 +1,224 @@ +package service + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "regexp" + "sort" + "strings" + "sync" + "time" + + "github.com/mhsanaei/3x-ui/v3/logger" +) + +type xrayMetricsState struct { + Enabled bool `json:"enabled"` + Listen string `json:"listen"` + Reason string `json:"reason,omitempty"` +} + +type ObsTagSnapshot struct { + Tag string `json:"tag"` + Alive bool `json:"alive"` + Delay int64 `json:"delay"` + LastSeenTime int64 `json:"lastSeenTime"` + LastTryTime int64 `json:"lastTryTime"` + UpdatedAt int64 `json:"updatedAt"` +} + +type XrayMetricsService struct { + settingService SettingService + + mu sync.RWMutex + state xrayMetricsState + client *http.Client + obsByTag map[string]ObsTagSnapshot +} + +var validObsTag = regexp.MustCompile(`^[a-zA-Z0-9._\-]+$`) + +func obsHistoryKey(tag string) string { + return "xrObs." + tag + ".delay" +} + +func newXrayMetricsClient() *http.Client { + return &http.Client{Timeout: 1500 * time.Millisecond} +} + +func (s *XrayMetricsService) getClient() *http.Client { + s.mu.Lock() + defer s.mu.Unlock() + if s.client == nil { + s.client = newXrayMetricsClient() + } + return s.client +} + +func (s *XrayMetricsService) State() xrayMetricsState { + s.mu.RLock() + defer s.mu.RUnlock() + return s.state +} + +func (s *XrayMetricsService) AggregateMetric(metric string, bucketSeconds, maxPoints int) []map[string]any { + return xrayMetrics.aggregate(metric, bucketSeconds, maxPoints) +} + +func (s *XrayMetricsService) ObservatorySnapshot() []ObsTagSnapshot { + s.mu.RLock() + defer s.mu.RUnlock() + out := make([]ObsTagSnapshot, 0, len(s.obsByTag)) + for _, v := range s.obsByTag { + out = append(out, v) + } + sort.Slice(out, func(i, j int) bool { return out[i].Tag < out[j].Tag }) + return out +} + +func (s *XrayMetricsService) HasObservatoryTag(tag string) bool { + if !validObsTag.MatchString(tag) { + return false + } + s.mu.RLock() + defer s.mu.RUnlock() + _, ok := s.obsByTag[tag] + return ok +} + +func (s *XrayMetricsService) AggregateObservatory(tag string, bucketSeconds, maxPoints int) []map[string]any { + if !validObsTag.MatchString(tag) { + return []map[string]any{} + } + return xrayMetrics.aggregate(obsHistoryKey(tag), bucketSeconds, maxPoints) +} + +func (s *XrayMetricsService) discoverListen() (string, error) { + tmpl, err := s.settingService.GetXrayConfigTemplate() + if err != nil { + return "", err + } + var parsed struct { + Metrics *struct { + Listen string `json:"listen"` + } `json:"metrics"` + } + if err := json.Unmarshal([]byte(tmpl), &parsed); err != nil { + return "", err + } + if parsed.Metrics == nil || strings.TrimSpace(parsed.Metrics.Listen) == "" { + return "", nil + } + return strings.TrimSpace(parsed.Metrics.Listen), nil +} + +type rawObsEntry struct { + Alive bool `json:"alive"` + Delay int64 `json:"delay"` + LastSeenTime int64 `json:"last_seen_time"` + LastTryTime int64 `json:"last_try_time"` + OutboundTag string `json:"outbound_tag"` +} + +func (s *XrayMetricsService) Sample(t time.Time) { + listen, err := s.discoverListen() + if err != nil { + s.setState(xrayMetricsState{Reason: err.Error()}) + return + } + if listen == "" { + s.setState(xrayMetricsState{Reason: "metrics block not configured in xray template"}) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 1500*time.Millisecond) + defer cancel() + url := fmt.Sprintf("http://%s/debug/vars", listen) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + s.setState(xrayMetricsState{Listen: listen, Reason: err.Error()}) + return + } + resp, err := s.getClient().Do(req) + if err != nil { + s.setState(xrayMetricsState{Listen: listen, Reason: err.Error()}) + return + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + s.setState(xrayMetricsState{Listen: listen, Reason: fmt.Sprintf("HTTP %d", resp.StatusCode)}) + return + } + + var payload struct { + MemStats struct { + HeapAlloc uint64 `json:"HeapAlloc"` + Sys uint64 `json:"Sys"` + HeapObjects uint64 `json:"HeapObjects"` + NumGC uint32 `json:"NumGC"` + PauseNs [256]uint64 `json:"PauseNs"` + } `json:"memstats"` + Observatory map[string]rawObsEntry `json:"observatory"` + } + if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { + s.setState(xrayMetricsState{Listen: listen, Reason: err.Error()}) + return + } + + xrayMetrics.append("xrAlloc", t, float64(payload.MemStats.HeapAlloc)) + xrayMetrics.append("xrSys", t, float64(payload.MemStats.Sys)) + xrayMetrics.append("xrHeapObjects", t, float64(payload.MemStats.HeapObjects)) + xrayMetrics.append("xrNumGC", t, float64(payload.MemStats.NumGC)) + var lastPause uint64 + if payload.MemStats.NumGC > 0 { + idx := (payload.MemStats.NumGC + 255) % 256 + lastPause = payload.MemStats.PauseNs[idx] + } + xrayMetrics.append("xrPauseNs", t, float64(lastPause)) + + s.applyObservatory(t, payload.Observatory) + s.setState(xrayMetricsState{Enabled: true, Listen: listen}) +} + +func (s *XrayMetricsService) applyObservatory(t time.Time, entries map[string]rawObsEntry) { + next := make(map[string]ObsTagSnapshot, len(entries)) + for key, e := range entries { + tag := e.OutboundTag + if tag == "" { + tag = key + } + if !validObsTag.MatchString(tag) { + continue + } + snap := ObsTagSnapshot{ + Tag: tag, + Alive: e.Alive, + Delay: e.Delay, + LastSeenTime: e.LastSeenTime, + LastTryTime: e.LastTryTime, + UpdatedAt: t.Unix(), + } + next[tag] = snap + xrayMetrics.append(obsHistoryKey(tag), t, float64(e.Delay)) + } + + s.mu.Lock() + for tag := range s.obsByTag { + if _, kept := next[tag]; !kept { + xrayMetrics.drop(obsHistoryKey(tag)) + } + } + s.obsByTag = next + s.mu.Unlock() +} + +func (s *XrayMetricsService) setState(st xrayMetricsState) { + s.mu.Lock() + s.state = st + s.mu.Unlock() + if !st.Enabled && st.Reason != "" { + logger.Debugf("xray metrics unavailable: %s", st.Reason) + } +} diff --git a/web/translation/ar-EG.json b/web/translation/ar-EG.json index 47152b57..7ab23c2c 100644 --- a/web/translation/ar-EG.json +++ b/web/translation/ar-EG.json @@ -143,6 +143,17 @@ "xrayErrorPopoverTitle": "حصل خطأ أثناء تشغيل Xray", "operationHours": "مدة التشغيل", "systemHistoryTitle": "تاريخ النظام", + "charts": "الرسوم البيانية", + "xrayMetricsTitle": "مقاييس Xray", + "xrayMetricsDisabled": "نقطة نهاية مقاييس Xray غير مهيأة", + "xrayMetricsHint": "أضف كتلة metrics على المستوى الأعلى في إعدادات xray مع tag باسم metrics_out و listen على 127.0.0.1:11111، ثم أعد تشغيل xray.", + "xrayObservatoryEmpty": "لا توجد بيانات Observatory بعد", + "xrayObservatoryHint": "أضف كتلة observatory إلى إعدادات xray مع قائمة وسوم outbound للفحص، ثم أعد تشغيل xray.", + "xrayObservatoryTagPlaceholder": "اختر outbound", + "xrayObservatoryAlive": "نشط", + "xrayObservatoryDead": "غير متصل", + "xrayObservatoryLastSeen": "آخر مشاهدة", + "xrayObservatoryLastTry": "آخر محاولة", "trendLast2Min": "آخر دقيقتين", "systemLoad": "تحميل النظام", "systemLoadDesc": "متوسط تحميل النظام في الدقائق 1, 5, و15", diff --git a/web/translation/en-US.json b/web/translation/en-US.json index bd1f882f..c8e08798 100644 --- a/web/translation/en-US.json +++ b/web/translation/en-US.json @@ -143,6 +143,17 @@ "xrayErrorPopoverTitle": "An error occurred while running Xray", "operationHours": "Uptime", "systemHistoryTitle": "System History", + "charts": "Charts", + "xrayMetricsTitle": "Xray Metrics", + "xrayMetricsDisabled": "Xray metrics endpoint not configured", + "xrayMetricsHint": "Add a top-level metrics block to the xray config with tag metrics_out and listen 127.0.0.1:11111, then restart xray.", + "xrayObservatoryEmpty": "No observatory data yet", + "xrayObservatoryHint": "Add an observatory block to the xray config listing the outbound tags to probe, then restart xray.", + "xrayObservatoryTagPlaceholder": "Select outbound", + "xrayObservatoryAlive": "Alive", + "xrayObservatoryDead": "Down", + "xrayObservatoryLastSeen": "Last seen", + "xrayObservatoryLastTry": "Last try", "trendLast2Min": "Last 2 minutes", "systemLoad": "System Load", "systemLoadDesc": "System load average for the past 1, 5, and 15 minutes", diff --git a/web/translation/es-ES.json b/web/translation/es-ES.json index 4fad3fdc..2e610b2b 100644 --- a/web/translation/es-ES.json +++ b/web/translation/es-ES.json @@ -143,6 +143,17 @@ "xrayErrorPopoverTitle": "Se produjo un error al ejecutar Xray", "operationHours": "Tiempo de Funcionamiento", "systemHistoryTitle": "Historial del Sistema", + "charts": "Gráficos", + "xrayMetricsTitle": "Métricas de Xray", + "xrayMetricsDisabled": "Endpoint de métricas de Xray no configurado", + "xrayMetricsHint": "Añade un bloque metrics de nivel superior a la configuración de xray con tag metrics_out y listen 127.0.0.1:11111, luego reinicia xray.", + "xrayObservatoryEmpty": "Aún no hay datos de Observatory", + "xrayObservatoryHint": "Añade un bloque observatory a la configuración de xray listando los tags de outbound a sondear, luego reinicia xray.", + "xrayObservatoryTagPlaceholder": "Seleccionar outbound", + "xrayObservatoryAlive": "Activo", + "xrayObservatoryDead": "Caído", + "xrayObservatoryLastSeen": "Visto por última vez", + "xrayObservatoryLastTry": "Último intento", "trendLast2Min": "Últimos 2 minutos", "systemLoad": "Carga del Sistema", "systemLoadDesc": "promedio de carga del sistema en los últimos 1, 5 y 15 minutos", diff --git a/web/translation/fa-IR.json b/web/translation/fa-IR.json index 6b75c54b..fc5ba7cd 100644 --- a/web/translation/fa-IR.json +++ b/web/translation/fa-IR.json @@ -143,6 +143,17 @@ "xrayErrorPopoverTitle": "خطا در هنگام اجرای Xray رخ داد", "operationHours": "مدتکارکرد", "systemHistoryTitle": "تاریخچه سیستم", + "charts": "نمودارها", + "xrayMetricsTitle": "متریکهای Xray", + "xrayMetricsDisabled": "نقطه پایانی متریکهای Xray پیکربندی نشده", + "xrayMetricsHint": "یک بلاک metrics در سطح بالای پیکربندی xray با tag برابر metrics_out و listen برابر 127.0.0.1:11111 اضافه کنید، سپس xray را راهاندازی مجدد کنید.", + "xrayObservatoryEmpty": "هنوز دادهای از Observatory دریافت نشده", + "xrayObservatoryHint": "یک بلاک observatory در پیکربندی xray اضافه کنید و outbound tagهایی که میخواهید بررسی شوند را لیست کنید، سپس xray را راهاندازی مجدد کنید.", + "xrayObservatoryTagPlaceholder": "انتخاب outbound", + "xrayObservatoryAlive": "فعال", + "xrayObservatoryDead": "غیرفعال", + "xrayObservatoryLastSeen": "آخرین مشاهده", + "xrayObservatoryLastTry": "آخرین تلاش", "trendLast2Min": "۲ دقیقه اخیر", "systemLoad": "بارسیستم", "systemLoadDesc": "میانگین بار سیستم برای 1، 5 و 15 دقیقه گذشته", diff --git a/web/translation/id-ID.json b/web/translation/id-ID.json index 4a189655..c651d64f 100644 --- a/web/translation/id-ID.json +++ b/web/translation/id-ID.json @@ -143,6 +143,17 @@ "xrayErrorPopoverTitle": "Terjadi kesalahan saat menjalankan Xray", "operationHours": "Waktu Aktif", "systemHistoryTitle": "Riwayat Sistem", + "charts": "Grafik", + "xrayMetricsTitle": "Metrik Xray", + "xrayMetricsDisabled": "Endpoint metrik Xray belum dikonfigurasi", + "xrayMetricsHint": "Tambahkan blok metrics tingkat atas ke konfigurasi xray dengan tag metrics_out dan listen 127.0.0.1:11111, lalu mulai ulang xray.", + "xrayObservatoryEmpty": "Belum ada data Observatory", + "xrayObservatoryHint": "Tambahkan blok observatory ke konfigurasi xray yang mencantumkan tag outbound untuk diuji, lalu mulai ulang xray.", + "xrayObservatoryTagPlaceholder": "Pilih outbound", + "xrayObservatoryAlive": "Aktif", + "xrayObservatoryDead": "Mati", + "xrayObservatoryLastSeen": "Terakhir terlihat", + "xrayObservatoryLastTry": "Percobaan terakhir", "trendLast2Min": "2 menit terakhir", "systemLoad": "Beban Sistem", "systemLoadDesc": "Rata-rata beban sistem selama 1, 5, dan 15 menit terakhir", diff --git a/web/translation/ja-JP.json b/web/translation/ja-JP.json index 5c2f3013..1daca2e5 100644 --- a/web/translation/ja-JP.json +++ b/web/translation/ja-JP.json @@ -143,6 +143,17 @@ "xrayErrorPopoverTitle": "Xrayの実行中にエラーが発生しました", "operationHours": "システム稼働時間", "systemHistoryTitle": "システム履歴", + "charts": "チャート", + "xrayMetricsTitle": "Xray メトリクス", + "xrayMetricsDisabled": "Xray メトリクスエンドポイントが設定されていません", + "xrayMetricsHint": "xray 設定にトップレベルの metrics ブロック(tag: metrics_out、listen: 127.0.0.1:11111)を追加し、xray を再起動してください。", + "xrayObservatoryEmpty": "Observatory データはまだありません", + "xrayObservatoryHint": "xray 設定に observatory ブロックを追加し、プローブする outbound タグを列挙してから xray を再起動してください。", + "xrayObservatoryTagPlaceholder": "Outbound を選択", + "xrayObservatoryAlive": "稼働中", + "xrayObservatoryDead": "停止", + "xrayObservatoryLastSeen": "最終確認", + "xrayObservatoryLastTry": "最終試行", "trendLast2Min": "直近2分", "systemLoad": "システム負荷", "systemLoadDesc": "過去1、5、15分間のシステム平均負荷", diff --git a/web/translation/pt-BR.json b/web/translation/pt-BR.json index cfec3c58..f639f230 100644 --- a/web/translation/pt-BR.json +++ b/web/translation/pt-BR.json @@ -143,6 +143,17 @@ "xrayErrorPopoverTitle": "Ocorreu um erro ao executar o Xray", "operationHours": "Tempo de Atividade", "systemHistoryTitle": "Histórico do Sistema", + "charts": "Gráficos", + "xrayMetricsTitle": "Métricas do Xray", + "xrayMetricsDisabled": "Endpoint de métricas do Xray não configurado", + "xrayMetricsHint": "Adicione um bloco metrics de nível superior à configuração do xray com tag metrics_out e listen 127.0.0.1:11111, depois reinicie o xray.", + "xrayObservatoryEmpty": "Ainda não há dados do Observatory", + "xrayObservatoryHint": "Adicione um bloco observatory à configuração do xray listando as tags de outbound a sondar, depois reinicie o xray.", + "xrayObservatoryTagPlaceholder": "Selecionar outbound", + "xrayObservatoryAlive": "Ativo", + "xrayObservatoryDead": "Inativo", + "xrayObservatoryLastSeen": "Visto pela última vez", + "xrayObservatoryLastTry": "Última tentativa", "trendLast2Min": "Últimos 2 minutos", "systemLoad": "Carga do Sistema", "systemLoadDesc": "Média de carga do sistema nos últimos 1, 5 e 15 minutos", diff --git a/web/translation/ru-RU.json b/web/translation/ru-RU.json index efb4ac9f..b2b88cfe 100644 --- a/web/translation/ru-RU.json +++ b/web/translation/ru-RU.json @@ -143,6 +143,17 @@ "xrayErrorPopoverTitle": "Ошибка при запуске Xray", "operationHours": "Время работы системы", "systemHistoryTitle": "История системы", + "charts": "Графики", + "xrayMetricsTitle": "Метрики Xray", + "xrayMetricsDisabled": "Конечная точка метрик Xray не настроена", + "xrayMetricsHint": "Добавьте блок metrics верхнего уровня в конфигурацию xray с tag metrics_out и listen 127.0.0.1:11111, затем перезапустите xray.", + "xrayObservatoryEmpty": "Данных Observatory пока нет", + "xrayObservatoryHint": "Добавьте блок observatory в конфигурацию xray с указанием тегов outbound для проверки, затем перезапустите xray.", + "xrayObservatoryTagPlaceholder": "Выберите outbound", + "xrayObservatoryAlive": "Активен", + "xrayObservatoryDead": "Недоступен", + "xrayObservatoryLastSeen": "Последняя активность", + "xrayObservatoryLastTry": "Последняя попытка", "trendLast2Min": "Последние 2 минуты", "systemLoad": "Нагрузка на систему", "systemLoadDesc": "Средняя загрузка системы за последние 1, 5 и 15 минут", diff --git a/web/translation/tr-TR.json b/web/translation/tr-TR.json index 3c8932c8..05136a79 100644 --- a/web/translation/tr-TR.json +++ b/web/translation/tr-TR.json @@ -143,6 +143,17 @@ "xrayErrorPopoverTitle": "Xray çalıştırılırken bir hata oluştu", "operationHours": "Çalışma Süresi", "systemHistoryTitle": "Sistem Geçmişi", + "charts": "Grafikler", + "xrayMetricsTitle": "Xray Metrikleri", + "xrayMetricsDisabled": "Xray metrik uç noktası yapılandırılmadı", + "xrayMetricsHint": "xray yapılandırmasına tag metrics_out ve listen 127.0.0.1:11111 olan üst düzey bir metrics bloğu ekleyin, sonra xray'i yeniden başlatın.", + "xrayObservatoryEmpty": "Henüz Observatory verisi yok", + "xrayObservatoryHint": "xray yapılandırmasına test edilecek outbound etiketlerini listeleyen bir observatory bloğu ekleyin, sonra xray'i yeniden başlatın.", + "xrayObservatoryTagPlaceholder": "Outbound seç", + "xrayObservatoryAlive": "Aktif", + "xrayObservatoryDead": "Kapalı", + "xrayObservatoryLastSeen": "Son görülme", + "xrayObservatoryLastTry": "Son deneme", "trendLast2Min": "Son 2 dakika", "systemLoad": "Sistem Yükü", "systemLoadDesc": "Geçmiş 1, 5 ve 15 dakika için sistem yük ortalaması", diff --git a/web/translation/uk-UA.json b/web/translation/uk-UA.json index 66839b55..eb451b14 100644 --- a/web/translation/uk-UA.json +++ b/web/translation/uk-UA.json @@ -143,6 +143,17 @@ "xrayErrorPopoverTitle": "Під час роботи Xray сталася помилка", "operationHours": "Час роботи", "systemHistoryTitle": "Історія системи", + "charts": "Графіки", + "xrayMetricsTitle": "Метрики Xray", + "xrayMetricsDisabled": "Кінцева точка метрик Xray не налаштована", + "xrayMetricsHint": "Додайте блок metrics верхнього рівня до конфігурації xray з tag metrics_out і listen 127.0.0.1:11111, потім перезапустіть xray.", + "xrayObservatoryEmpty": "Даних Observatory ще немає", + "xrayObservatoryHint": "Додайте блок observatory до конфігурації xray зі списком outbound тегів для перевірки, потім перезапустіть xray.", + "xrayObservatoryTagPlaceholder": "Виберіть outbound", + "xrayObservatoryAlive": "Активний", + "xrayObservatoryDead": "Недоступний", + "xrayObservatoryLastSeen": "Остання активність", + "xrayObservatoryLastTry": "Остання спроба", "trendLast2Min": "Останні 2 хвилини", "systemLoad": "Завантаження системи", "systemLoadDesc": "Середнє завантаження системи за останні 1, 5 і 15 хвилин", diff --git a/web/translation/vi-VN.json b/web/translation/vi-VN.json index 5353d6cc..8e0ff6e6 100644 --- a/web/translation/vi-VN.json +++ b/web/translation/vi-VN.json @@ -143,6 +143,17 @@ "xrayErrorPopoverTitle": "Đã xảy ra lỗi khi chạy Xray", "operationHours": "Thời gian hoạt động", "systemHistoryTitle": "Lịch sử hệ thống", + "charts": "Biểu đồ", + "xrayMetricsTitle": "Chỉ số Xray", + "xrayMetricsDisabled": "Điểm cuối chỉ số Xray chưa được cấu hình", + "xrayMetricsHint": "Thêm khối metrics cấp cao nhất vào cấu hình xray với tag là metrics_out và listen là 127.0.0.1:11111, sau đó khởi động lại xray.", + "xrayObservatoryEmpty": "Chưa có dữ liệu Observatory", + "xrayObservatoryHint": "Thêm khối observatory vào cấu hình xray liệt kê các tag outbound cần kiểm tra, sau đó khởi động lại xray.", + "xrayObservatoryTagPlaceholder": "Chọn outbound", + "xrayObservatoryAlive": "Hoạt động", + "xrayObservatoryDead": "Ngừng", + "xrayObservatoryLastSeen": "Lần cuối thấy", + "xrayObservatoryLastTry": "Lần thử cuối", "trendLast2Min": "2 phút gần nhất", "systemLoad": "Tải hệ thống", "systemLoadDesc": "trung bình tải hệ thống trong 1, 5 và 15 phút qua", diff --git a/web/translation/zh-CN.json b/web/translation/zh-CN.json index 2c501498..e27c3652 100644 --- a/web/translation/zh-CN.json +++ b/web/translation/zh-CN.json @@ -143,6 +143,17 @@ "xrayErrorPopoverTitle": "运行Xray时发生错误", "operationHours": "系统正常运行时间", "systemHistoryTitle": "系统历史", + "charts": "图表", + "xrayMetricsTitle": "Xray 指标", + "xrayMetricsDisabled": "未配置 Xray 指标端点", + "xrayMetricsHint": "在 xray 配置中添加顶级 metrics 块,tag 为 metrics_out,listen 为 127.0.0.1:11111,然后重启 xray。", + "xrayObservatoryEmpty": "暂无 Observatory 数据", + "xrayObservatoryHint": "在 xray 配置中添加 observatory 块,列出要探测的出站 tag,然后重启 xray。", + "xrayObservatoryTagPlaceholder": "选择出站", + "xrayObservatoryAlive": "在线", + "xrayObservatoryDead": "离线", + "xrayObservatoryLastSeen": "最后在线", + "xrayObservatoryLastTry": "最后尝试", "trendLast2Min": "最近 2 分钟", "systemLoad": "系统负载", "systemLoadDesc": "过去 1、5 和 15 分钟的系统平均负载", diff --git a/web/translation/zh-TW.json b/web/translation/zh-TW.json index 2e1fe327..78f26dee 100644 --- a/web/translation/zh-TW.json +++ b/web/translation/zh-TW.json @@ -143,6 +143,17 @@ "xrayErrorPopoverTitle": "執行Xray時發生錯誤", "operationHours": "系統正常執行時間", "systemHistoryTitle": "系統歷史", + "charts": "圖表", + "xrayMetricsTitle": "Xray 指標", + "xrayMetricsDisabled": "未設定 Xray 指標端點", + "xrayMetricsHint": "在 xray 設定中加入頂層 metrics 區塊,tag 為 metrics_out,listen 為 127.0.0.1:11111,然後重啟 xray。", + "xrayObservatoryEmpty": "尚無 Observatory 資料", + "xrayObservatoryHint": "在 xray 設定中加入 observatory 區塊,列出要探測的出站 tag,然後重啟 xray。", + "xrayObservatoryTagPlaceholder": "選擇出站", + "xrayObservatoryAlive": "在線", + "xrayObservatoryDead": "離線", + "xrayObservatoryLastSeen": "最後在線", + "xrayObservatoryLastTry": "最後嘗試", "trendLast2Min": "最近 2 分鐘", "systemLoad": "系統負載", "systemLoadDesc": "過去 1、5 和 15 分鐘的系統平均負載",