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>
- Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9
- Add eslint.config.js (flat config required by ESLint 10) with
vue3-recommended rules, sensible defaults, and exemptions for the
project's existing formatting style
- Drop --ext from the lint script (removed in ESLint 10)
- vue/no-mutating-props is left off because the form-modal pattern
ports straight from Vue 2 (parent passes a reactive object, child
mutates it); a real fix is an architectural rewire, separate task
Lint warning cleanup:
- utils/index.js: var -> let/const in the X25519 routines, replace
obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...)
- Remove unused imports (reactive, ref, Inbound) in ClientFormModal,
InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal,
SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS,
fetchAll, isSocks, isHTTP, _antdAlgorithm)
- XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but
not declared)
- RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t)
- Drop stale eslint-disable directives (no-new, no-unused-vars)
- OutboundsTab/InboundList: drop redundant initial null assigns
- InboundInfoModal/OutboundFormModal: explicit eslint-disable for the
intentional local-ref-shadows-prop pattern in modal drafts
`npm run lint` now passes with 0 errors and 0 warnings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Push the inbound table's expand chevron away from the left edge with
margin-inline + cell padding so it isn't flush against the corner
- Move "Timeframe: …" caption above the chart (was below); restore
the line that the previous edit removed
- Fix x-axis time labels being clipped at the bottom of the cpu chart
— the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG
viewBox height (220); dropped to +14 so labels sit at y=214 with
room for descenders
- Move the SVG axis text colors out of <style scoped> into a global
block — Vue's scoped CSS doesn't always hash-attribute SVG <text>
descendants, so the dark-mode overrides via :global() weren't
matching; bumped opacity 0.55 → 0.85 for legibility on navy/black
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Drop --bg-page from #21242a (lighter than the cards) to #050505 in
ultra-dark across index/sub/settings/inbounds/xray, so cards
consistently elevate over the page
- Hide the inline sider's children + collapse-trigger and zero its
width below 768px; the floating drawer-handle remains the menu
trigger
- Inbounds page mobile pass: tighten content-area + card padding;
flex-wrap the filter bar instead of stacking; shrink table cell
padding so all 4 mobile columns fit; bump expand / action / info
icon hit targets
- Per-client expand row on mobile: soft-tinted rounded cards instead
of hairline borders, larger action / info touch targets, more
legible email typography, bigger status badge dot
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- IntlUtil.formatDate accepts an optional calendar arg; appends the
BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI
languages, not just fa-IR
- Plumb the panel's datepicker setting into the SubPage via the Go
injection (window.__SUB_PAGE_DATA__.datepicker)
- Panel pages (inbound list/info, client row, xray log) read the same
setting through the useDatepicker composable so the whole panel
stays consistent
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two follow-up tweaks to the dashboard gauges:
- AD-Vue scales the percent text from the SVG, not from :width, so
the 90px gauges still rendered the number at ~27px. Pin
.ant-progress-text to 14px via :deep() and trim the gauge to 70px
(60px on mobile) so the whole card stays compact.
- The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was
invisible on the light-theme card. Pass an explicit
rgba(128,128,128,0.25) trail-color so the unfinished portion is
visible under both light and dark themes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff
card background that the cards faded into the page. Bump it to #e6e8ec
(a more visibly distinct gray) so cards lift cleanly off the surface.
Dark and ultra-dark stay where they were.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's
default 120px width which made the percent text balloon to ~36px.
Drop to 90px (70px on mobile) so the gauge fits the rest of the card.
- The CurTotal.color thresholds still hardcoded the legacy teal/orange
palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary /
warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges
match the rest of the panel under both light and dark themes.
- XrayStatusCard's running-animation badge ring also still pointed at
the deleted --color-primary-100 var; hardcode the new primary blue.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The legacy panel CSS (custom.min.css ported as legacy.css) tinted every
non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary)
overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue —
producing the mixed blue/green button look on dark mode. Drop legacy.css
entirely and let AD-Vue 4's algorithms own the palette.
Centralize antdThemeConfig in useTheme.js so every page resolves to the
same source of truth (light = defaultAlgorithm, dark = darkAlgorithm,
ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/
Elevated tokens). Each page's <a-config-provider> now imports the
shared computed instead of defining its own copy.
Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Index dashboard regains the 8 cards that were lost in the SPA port
(3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed,
Total Data, IP Addresses, Connection Stats), plus a Config button that
shows the live xray config.json. Version display falls back through
panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank.
- Xray config no longer hangs on load: useXraySetting surfaces failures
instead of leaving a perpetual spinner, and the Vite dev proxy stops
hijacking POST requests to migrated routes (only GETs get bypassed).
- Inbound page no longer throws __asyncLoader/emitsOptions errors —
inbound.js was missing imports (NumberFormatter, SizeFormatter,
Wireguard) and InboundList kept emitting after unmount.
- Login round-trip works after logout: a public /csrf-token endpoint
bootstraps the SPA before authentication, axios caches the token
module-level, and the dev 401 handler navigates to /login.html
instead of reloading the dashboard into a redirect loop.
- legacy.css mirrors the legacy panel's surface/text variables so dark
and ultra-dark themes match main; every SPA entry imports it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes the index page's i18n coverage. Combined with the page-chrome
commit, every label users see on the dashboard is now sourced from
the TOML translation files.
Per file:
- IndexPage.vue: loading-spinner tip (initial + dynamic).
- BackupModal.vue: modal title, both list-item titles + descriptions
("Back up" / "Restore"), in-flight busy tips ("Importing database…"
/ "Restarting panel…").
- PanelUpdateModal.vue: modal title, update-available alert,
current/latest version row labels, "Up to date" tag + label,
primary action button. Modal.confirm now uses the translated
panelUpdateDialog / panelUpdateDialogDesc with #version#
substitution; success toast uses panelUpdateStartedPopover.
- LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/
Error log-level options stay literal — they're xray's wire values,
not user-facing labels (matches the existing settings-page choice).
- XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay
literal for the same reason.
- VersionModal.vue: modal title + xray-switch alert + per-file
tooltip + "Update all" button + custom-geo collapse header. The
Modal.confirm flows for switchXrayVersion + updateGeofile use
translated dialog/desc with #version# / #filename# substitution.
- CpuHistoryModal.vue: title slot.
- CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons,
every column title (computed for live locale), copy/edit/download/
delete tooltips, copy toast, delete-confirm modal, empty-state
text.
- CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/
Alias/URL field labels, alias placeholder, all three validation
toasts.
Total: ~50 strings localised across 8 index-page files. The Hello /
Welcome login headline cycle and a handful of literal xray wire
values (Direct/Blocked/Proxy/log levels) are intentionally kept
hardcoded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces hardcoded English with t() calls in the components every
user sees on every page load. The translations themselves come from
the existing TOML files via the sync script — no new strings, no
new locale keys.
Per component:
- AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings /
xray / logout). Computed so the sidebar re-renders when the
cookie-driven locale flips on reload.
- IndexPage.vue: Quick actions card title + Logs / Backup / Up-to-
date / Update buttons.
- StatusCard.vue: CPU / Memory / Swap / Storage labels +
logical-processors / frequency tooltips.
- XrayStatusCard.vue: card title + error popover header + Stop /
Restart / Switch xray action labels (kept the v-prefix version
string as-is — it's content, not a label).
- SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons +
unsaved-changes warning.
- XrayPage.vue: 6 tab titles + Save / Restart-xray buttons +
unsaved-changes warning.
- InboundsPage.vue: 5 summary-stat card titles.
- InboundList.vue: 10 column titles (computed for live locale),
Add inbound / General actions buttons + every dropdown menu item,
search placeholder, filter radio labels, popover titles
(disabled / depleted / depleting / online), traffic + info
popover row labels.
Total: ~75 strings localised across 8 files. The remaining English
labels live in the per-tab settings forms, the form modals
(Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and
the per-row table cell helpers — all incremental work that doesn't
touch infrastructure.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the third collapse panel ("Custom geo") that lets users register
external geosite/geoip files referenced by routing rules via
ext:<filename>:tag. Backend endpoints are unchanged.
- CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list
with per-row edit, download (refetch), and delete actions, plus an
Add button and Update-all. Lazy-loads the list when the parent
collapse opens this panel — closed panels don't fetch.
- CustomGeoFormModal.vue: shared add/edit form with the same alias
regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias
are immutable when editing — backend rejects changes anyway.
- ext:<filename>:tag value is click-to-copy via ClipboardManager.
- Relative time is computed inline (no moment dep); tooltip shows the
absolute timestamp.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wires up the three remaining dashboard buttons that were stubbed in
5c-iv (a): the CPU history button on StatusCard, the xray-logs button
in XrayStatusCard's error popover and ipLimitEnable action, and the
"Switch xray" button in XrayStatusCard's action footer.
- Sparkline.vue: shared SVG line chart (composition-API port of the
inline Vue 2 component). Per-instance gradient id avoids defs
collisions between sparklines on the same page.
- CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives
GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline.
- XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes;
POST /panel/api/server/xraylogs/{rows} returns access-log entries
rendered as a colored HTML table; download button serializes to text.
- VersionModal.vue: collapse with Xray panel (radio list of versions
from getXrayVersion, install via installXray/{version}) and Geofiles
panel (per-file reload + Update all). CustomGeo collapse panel is
Phase 5c-v.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds three of the six dashboard modals plus a Quick Actions card
that surfaces them. The remaining three (xray logs, version picker,
CPU history sparkline) ship in 5c-iv-b.
- PanelUpdateModal.vue — current vs latest version, "update now"
button. Confirm dialog → POST /panel/api/server/updatePanel,
then poll /server/status for up to 90s until the new panel
answers, then reload.
- LogModal.vue — panel logs viewer. Filters: rows (10-500), level
(debug/info/notice/warning/error), syslog toggle. Auto-fetches
on open and on every filter change. Color-coded timestamps and
levels via inline span styles. Download button writes the raw
log to x-ui.log via FileManager.downloadTextFile.
- BackupModal.vue — db export (window.location to /getDb) and
import (FormData upload to /importDB, then panel restart + reload).
- Quick Actions card surfaces Logs / Backup / Update buttons and
shows an orange update badge (extra slot) when an update is
available.
Modal-busy pattern: long-running operations (update, import) emit
a `busy` event with a tip; IndexPage flips its a-spin overlay so the
user sees a loading message while the panel is restarting.
AD-Vue 4 changes:
- v-model on <a-modal> renamed to v-model:open
- v-model on <a-input>/<a-select>/<a-checkbox> uses the named
v-model:value / v-model:checked pattern
- <a-icon type="..."> dropped — explicit Ant icon imports
(BarsOutlined, CloudServerOutlined, CloudDownloadOutlined,
DownloadOutlined, UploadOutlined, SyncOutlined)
- Modal.confirm() replaces this.$confirm() since setup() has no `this`
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
XrayStatusCard.vue renders the right-hand card on the dashboard:
- Title with mobile-only version tag (matches the legacy collapse)
- Animated badge for the running/stop/error states. The pulsing dot
comes from xray-pulse keyframes (renamed from runningAnimation in
legacy custom.min.css). Color rings on the badge use the legacy's
per-state border-color overrides on .ant-badge-status-processing.
- Error state replaces the badge with a popover that surfaces the
multi-line errorMsg + a logs shortcut.
- Action row at the bottom: optional logs (when ipLimitEnable),
stop, restart, and version switch.
IndexPage now wires:
- POST /panel/api/server/stopXrayService and /restartXrayService,
followed by a refresh() so the status card reflects the new state
without waiting for the next poll tick
- POST /panel/setting/defaultSettings to read ipLimitEnable
- Stub handlers for the panel-logs / xray-logs / version-switch /
cpu-history modals — those land in 5c-iv
AD-Vue 4 changes hit on this card:
- <a-icon type="bars|poweroff|reload|tool"> → explicit
BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined
- <span slot="title|content"> → <template #title|#content>
- The .xray-*-animation classes ship as global <style> (not scoped)
so they pierce AD-Vue's internal .ant-badge-status-* DOM.
i18n still hardcoded English; Phase 7 wires vue-i18n.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the CPU / memory / swap / disk dashboard cards to IndexPage,
backed by a useStatus() composable that polls /panel/api/server/status
every 2 s and a Status / CurTotal model ported from the legacy inline
classes in index.html.
- models/status.js — Status & CurTotal classes (CurTotal exposes
reactive .percent and .color computed-style getters; Status maps
the API payload + xray state to color/message strings)
- composables/useStatus.js — 2s polling with shallowRef so each fetch
swaps the whole Status object atomically. WebSocket integration
intentionally deferred — the legacy panel falls back to this same
2s polling when its websocket drops, so we ship the proven path
first and add WS on top in a later sub-phase.
- pages/index/StatusCard.vue — four a-progress dashboard widgets in
a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a
history button; the modal it opens is part of 5c-iv.
- IndexPage now consumes both, plus useMediaQuery so the layout
responds to viewport changes.
AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in
favor of explicit AreaChartOutlined / HistoryOutlined imports.
<a-tooltip slot="title"> → <template #title>.
i18n strings still hardcoded English (Phase 7 wires up vue-i18n).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the smoke-test App.vue with a real IndexPage shell so the
/index.html route now boots the actual dashboard layout in Vue 3:
- a-config-provider drives AD-Vue 4's dark algorithm from useTheme
(same pattern as LoginPage)
- AppSidebar (Phase 5b component) is wired in with basePath +
requestUri props
- a-spin loading state with placeholder card while we build out the
rest of the page
- Page palette mirrors the legacy: light #f0f2f5, dark #0a1222
(--dark-color-background), ultra-dark #21242a
The 1,805-line legacy index.html is too big for one commit. Split
into five sub-phases on the todo list: ii) status cards + /server/status
polling, iii) xray status card, iv) logs/backup/panel-update modals,
v) custom-geo section.
frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold)
are removed — both purposes now served by IndexPage and index.js.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>