Replaces the legacy polling + manual-refresh model with WebSocket pushes
across the three live-data pages. The hub already broadcast traffic /
client_stats / outbounds; this wires the frontend to consume them and
adds a new `nodes` channel for the heartbeat job's snapshot.
Frontend
- new useWebSocket composable: page-scoped singleton WebSocketClient,
lifecycle-managed on/off, leaves disconnect to page-unload
- inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent
/ applyInvalidate that merge counters and online/lastOnline in place;
InboundsPage subscribes; InboundList drops the auto-refresh popover,
the refresh button, and the now-unused refreshing prop
- xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage
subscribes; OutboundsTab drops the refresh button + emit
- nodes: useNodes gains applyNodesEvent and stops the 5s
setInterval/visibilitychange polling; NodesPage subscribes;
NodeList drops the refresh button and ReloadOutlined import
Backend
- web/websocket: new MessageTypeNodes + BroadcastNodes notifier
- node_heartbeat_job: after wg.Wait(), reload the table once and
BroadcastNodes(updated). Gated on websocket.HasClients() so a panel
with no open browser doesn't spend the DB read
Bug fixes spotted in this pass
- websocket.js #buildUrl defaulted basePath to '' when the global was
missing (dev mode), producing `ws://host:portws` and a SyntaxError
on the WebSocket constructor. Fall back to '/' and ensure leading
slash.
- vite.config.js: forward /ws to ws://localhost:2053 with ws:true so
dev (5173) reaches the Go backend's WebSocket
- NodeFormModal: a-input-password's visibilityToggle is Boolean in
AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`)
triggered a Vue prop-type warning. Drop the override (default true
shows the eye icon and toggles internally) and remove the orphaned
tokenVisible ref
Translations
- pages.inbounds.autoRefresh / autoRefreshInterval: removed from all
13 locales (UI gone)
- pages.nodes.refresh: removed from all 13 locales (UI gone)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Backend
- web/service/metric_history.go: generic in-memory ring buffer with two
singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15)
and per-node (cpu/mem) keyed by node id
- ServerService.AppendStatusSample writes all 8 metrics every 2s on the
same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat
- NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so
offline gaps render as missing data, not phantom dips
- New routes: GET /panel/api/server/history/:metric/:bucket and
GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted
Frontend
- Sparkline component generalized: arbitrary value range (auto-scale
when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s,
client counts, load averages
- SystemHistoryModal replaces CpuHistoryModal with tabs for every
metric; opened from a tag on the 3X-UI card next to Documentation
- NodeHistoryPanel: expandable row on the Nodes table showing per-node
CPU and Mem trends, refreshed every 15s
Localization
- Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node,
deployTo, localPanel} and the entire pages.nodes block (51 keys
including statusValues + toasts) into all 11 non-en/fa locales:
ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN,
zh-CN, zh-TW
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Vue SPA had been reading from frontend/src/locales/*.json while the
Go binary still loaded web/translation/translate.*.toml — and a
sync-locales.mjs pre-build step kept the two in lockstep, with TOML as
the source of truth. Now that go-i18n v2.6.1 already flattens nested
JSON via recGetMessages/addChildMessages, both runtimes can share one
file per locale.
- Move the 13 nested-JSON locale files to web/translation/<lang>.json
so they live alongside the Go //go:embed translation/* directive.
- Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal
(and drop the pelletier/go-toml import — it's now indirect-only).
Confirmed via a smoke test that pages.index.cpu, subscription.title,
tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR,
ru-RU, and zh-CN.
- Repoint Vue's i18n loader at the new path (../../../web/translation/
*.json glob) and drop the moved-here pathDelimiter comment that no
longer applies.
- Delete the 13 legacy translate.*.toml files and the sync-locales.mjs
script + its npm pre-script hooks (predev/prebuild/i18n:sync). The
Telegram bot and subscription page still get their messages because
they were reading the same MessageIDs the JSON files now produce.
- Update copilot-instructions.md so the next contributor knows where
the canonical translation files live.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 18:05:56 +02:00
Renamed from frontend/src/locales/zh-TW.json (Browse further)